EventState.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF 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. import org.hipparchus.analysis.UnivariateFunction;
  19. import org.hipparchus.analysis.solvers.BracketedUnivariateSolver;
  20. import org.hipparchus.analysis.solvers.BracketedUnivariateSolver.Interval;
  21. import org.hipparchus.analysis.solvers.BracketingNthOrderBrentSolver;
  22. import org.hipparchus.exception.MathRuntimeException;
  23. import org.hipparchus.util.FastMath;
  24. import org.hipparchus.util.Precision;
  25. import org.orekit.errors.OrekitException;
  26. import org.orekit.errors.OrekitExceptionWrapper;
  27. import org.orekit.errors.OrekitInternalError;
  28. import org.orekit.propagation.SpacecraftState;
  29. import org.orekit.propagation.events.handlers.EventHandler;
  30. import org.orekit.propagation.sampling.OrekitStepInterpolator;
  31. import org.orekit.time.AbsoluteDate;

  32. import java.io.Serializable;

  33. /** This class handles the state for one {@link EventDetector
  34.  * event detector} during integration steps.
  35.  *
  36.  * <p>This class is heavily based on the class with the same name from the
  37.  * Hipparchus library. The changes performed consist in replacing
  38.  * raw types (double and double arrays) with space dynamics types
  39.  * ({@link AbsoluteDate}, {@link SpacecraftState}).</p>
  40.  * <p>Each time the propagator proposes a step, the event detector
  41.  * should be checked. This class handles the state of one detector
  42.  * during one propagation step, with references to the state at the
  43.  * end of the preceding step. This information is used to determine if
  44.  * the detector should trigger an event or not during the proposed
  45.  * step (and hence the step should be reduced to ensure the event
  46.  * occurs at a bound rather than inside the step).</p>
  47.  * @author Luc Maisonobe
  48.  * @param <T> class type for the generic version
  49.  */
  50. public class EventState<T extends EventDetector> implements Serializable {

  51.     /** Serializable version identifier. */
  52.     private static final long serialVersionUID = 4489391420715269318L;

  53.     /** Event detector. */
  54.     private T detector;

  55.     /** Time of the previous call to g. */
  56.     private AbsoluteDate lastT;

  57.     /** Value from the previous call to g. */
  58.     private double lastG;

  59.     /** Time at the beginning of the step. */
  60.     private AbsoluteDate t0;

  61.     /** Value of the event detector at the beginning of the step. */
  62.     private double g0;

  63.     /** Simulated sign of g0 (we cheat when crossing events). */
  64.     private boolean g0Positive;

  65.     /** Indicator of event expected during the step. */
  66.     private boolean pendingEvent;

  67.     /** Occurrence time of the pending event. */
  68.     private AbsoluteDate pendingEventTime;

  69.     /**
  70.      * Time to stop propagation if the event is a stop event. Used to enable stopping at
  71.      * an event and then restarting after that event.
  72.      */
  73.     private AbsoluteDate stopTime;

  74.     /** Time after the current event. */
  75.     private AbsoluteDate afterEvent;

  76.     /** Value of the g function after the current event. */
  77.     private double afterG;

  78.     /** The earliest time considered for events. */
  79.     private AbsoluteDate earliestTimeConsidered;

  80.     /** Integration direction. */
  81.     private boolean forward;

  82.     /** Variation direction around pending event.
  83.      *  (this is considered with respect to the integration direction)
  84.      */
  85.     private boolean increasing;

  86.     /** Simple constructor.
  87.      * @param detector monitored event detector
  88.      */
  89.     public EventState(final T detector) {
  90.         this.detector     = detector;

  91.         // some dummy values ...
  92.         lastT                  = AbsoluteDate.PAST_INFINITY;
  93.         lastG                  = Double.NaN;
  94.         t0                     = null;
  95.         g0                     = Double.NaN;
  96.         g0Positive             = true;
  97.         pendingEvent           = false;
  98.         pendingEventTime       = null;
  99.         stopTime               = null;
  100.         increasing             = true;
  101.         earliestTimeConsidered = null;
  102.         afterEvent             = null;
  103.         afterG                 = Double.NaN;

  104.     }

  105.     /** Get the underlying event detector.
  106.      * @return underlying event detector
  107.      */
  108.     public T getEventDetector() {
  109.         return detector;
  110.     }

  111.     /** Initialize event handler at the start of a propagation.
  112.      * <p>
  113.      * This method is called once at the start of the propagation. It
  114.      * may be used by the event handler to initialize some internal data
  115.      * if needed.
  116.      * </p>
  117.      * @param s0 initial state
  118.      * @param t target time for the integration
  119.      *
  120.      * @throws OrekitException if some specific error occurs
  121.      */
  122.     public void init(final SpacecraftState s0,
  123.                      final AbsoluteDate t) throws OrekitException {
  124.         detector.init(s0, t);
  125.         lastT = AbsoluteDate.PAST_INFINITY;
  126.         lastG = Double.NaN;
  127.     }

  128.     /** Compute the value of the switching function.
  129.      * This function must be continuous (at least in its roots neighborhood),
  130.      * as the integrator will need to find its roots to locate the events.
  131.      * @param s the current state information: date, kinematics, attitude
  132.      * @return value of the switching function
  133.      * @exception OrekitException if some specific error occurs
  134.      */
  135.     private double g(final SpacecraftState s) throws OrekitException {
  136.         if (!s.getDate().equals(lastT)) {
  137.             lastT = s.getDate();
  138.             lastG = detector.g(s);
  139.         }
  140.         return lastG;
  141.     }

  142.     /** Reinitialize the beginning of the step.
  143.      * @param interpolator interpolator valid for the current step
  144.      * @exception OrekitException if the event detector
  145.      * value cannot be evaluated at the beginning of the step
  146.      */
  147.     public void reinitializeBegin(final OrekitStepInterpolator interpolator)
  148.         throws OrekitException {
  149.         forward = interpolator.isForward();
  150.         final SpacecraftState s0 = interpolator.getPreviousState();
  151.         this.t0 = s0.getDate();
  152.         g0 = g(s0);
  153.         while (g0 == 0) {
  154.             // extremely rare case: there is a zero EXACTLY at interval start
  155.             // we will use the sign slightly after step beginning to force ignoring this zero
  156.             // try moving forward by half a convergence interval
  157.             final double dt = (forward ? 0.5 : -0.5) * detector.getThreshold();
  158.             AbsoluteDate startDate = t0.shiftedBy(dt);
  159.             // if convergence is too small move an ulp
  160.             if (t0.equals(startDate)) {
  161.                 startDate = nextAfter(startDate);
  162.             }
  163.             t0 = startDate;
  164.             g0 = g(interpolator.getInterpolatedState(t0));
  165.         }
  166.         g0Positive = g0 > 0;
  167.         // "last" event was increasing
  168.         increasing = g0Positive;
  169.     }

  170.     /** Evaluate the impact of the proposed step on the event detector.
  171.      * @param interpolator step interpolator for the proposed step
  172.      * @return true if the event detector triggers an event before
  173.      * the end of the proposed step (this implies the step should be
  174.      * rejected)
  175.      * @exception OrekitException if the switching function
  176.      * cannot be evaluated
  177.      * @exception MathRuntimeException if an event cannot be located
  178.      */
  179.     public boolean evaluateStep(final OrekitStepInterpolator interpolator)
  180.         throws OrekitException, MathRuntimeException {

  181.         forward = interpolator.isForward();
  182.         final SpacecraftState s1 = interpolator.getCurrentState();
  183.         final AbsoluteDate t1 = s1.getDate();
  184.         final double dt = t1.durationFrom(t0);
  185.         if (FastMath.abs(dt) < detector.getThreshold()) {
  186.             // we cannot do anything on such a small step, don't trigger any events
  187.             return false;
  188.         }
  189.         // number of points to check in the current step
  190.         final int n = FastMath.max(1, (int) FastMath.ceil(FastMath.abs(dt) / detector.getMaxCheckInterval()));
  191.         final double h = dt / n;


  192.         AbsoluteDate ta = t0;
  193.         double ga = g0;
  194.         for (int i = 0; i < n; ++i) {

  195.             // evaluate handler value at the end of the substep
  196.             final AbsoluteDate tb = (i == n - 1) ? t1 : t0.shiftedBy((i + 1) * h);
  197.             final double gb = g(interpolator.getInterpolatedState(tb));

  198.             // check events occurrence
  199.             if (gb == 0.0 || (g0Positive ^ (gb > 0))) {
  200.                 // there is a sign change: an event is expected during this step
  201.                 if (findRoot(interpolator, ta, ga, tb, gb)) {
  202.                     return true;
  203.                 }
  204.             } else {
  205.                 // no sign change: there is no event for now
  206.                 ta = tb;
  207.                 ga = gb;
  208.             }

  209.         }

  210.         // no event during the whole step
  211.         pendingEvent     = false;
  212.         pendingEventTime = null;
  213.         return false;

  214.     }

  215.     /**
  216.      * Find a root in a bracketing interval.
  217.      *
  218.      * <p> When calling this method one of the following must be true. Either ga == 0, gb
  219.      * == 0, (ga < 0  and gb > 0), or (ga > 0 and gb < 0).
  220.      *
  221.      * @param interpolator that covers the interval.
  222.      * @param ta           earliest possible time for root.
  223.      * @param ga           g(ta).
  224.      * @param tb           latest possible time for root.
  225.      * @param gb           g(tb).
  226.      * @return if a zero crossing was found.
  227.      * @throws OrekitException if the event detector throws one
  228.      */
  229.     private boolean findRoot(final OrekitStepInterpolator interpolator,
  230.                              final AbsoluteDate ta, final double ga,
  231.                              final AbsoluteDate tb, final double gb)
  232.             throws OrekitException {
  233.         // check there appears to be a root in [ta, tb]
  234.         check(ga == 0.0 || gb == 0.0 || (ga > 0.0 && gb < 0.0) || (ga < 0.0 && gb > 0.0));

  235.         final double convergence = detector.getThreshold();
  236.         final int maxIterationCount = detector.getMaxIterationCount();
  237.         final BracketedUnivariateSolver<UnivariateFunction> solver =
  238.                 new BracketingNthOrderBrentSolver(0, convergence, 0, 5);

  239.         // event time, just at or before the actual root.
  240.         AbsoluteDate beforeRootT = null;
  241.         double beforeRootG = Double.NaN;
  242.         // time on the other side of the root.
  243.         // Initialized the the loop below executes once.
  244.         AbsoluteDate afterRootT = ta;
  245.         double afterRootG = 0.0;

  246.         // check for some conditions that the root finders don't like
  247.         // these conditions cannot not happen in the loop below
  248.         // the ga == 0.0 case is handled by the loop below
  249.         if (ta.equals(tb)) {
  250.             // both non-zero but times are the same. Probably due to reset state
  251.             beforeRootT = ta;
  252.             beforeRootG = ga;
  253.             afterRootT = shiftedBy(beforeRootT, convergence);
  254.             afterRootG = g(interpolator.getInterpolatedState(afterRootT));
  255.         } else if (ga != 0.0 && gb == 0.0) {
  256.             // hard: ga != 0.0 and gb == 0.0
  257.             // look past gb by up to convergence to find next sign
  258.             // throw an exception if g(t) = 0.0 in [tb, tb + convergence]
  259.             beforeRootT = tb;
  260.             beforeRootG = gb;
  261.             afterRootT = shiftedBy(beforeRootT, convergence);
  262.             afterRootG = g(interpolator.getInterpolatedState(afterRootT));
  263.         } else if (ga != 0.0) {
  264.             final double newGa = g(interpolator.getInterpolatedState(ta));
  265.             if (ga > 0 != newGa > 0) {
  266.                 // both non-zero, step sign change at ta, possibly due to reset state
  267.                 beforeRootT = ta;
  268.                 beforeRootG = newGa;
  269.                 afterRootT = minTime(shiftedBy(beforeRootT, convergence), tb);
  270.                 afterRootG = g(interpolator.getInterpolatedState(afterRootT));
  271.             }
  272.         }

  273.         // loop to skip through "fake" roots, i.e. where g(t) = g'(t) = 0.0
  274.         // executed once if we didn't hit a special case above
  275.         AbsoluteDate loopT = ta;
  276.         double loopG = ga;
  277.         while ((afterRootG == 0.0 || afterRootG > 0.0 == g0Positive) &&
  278.                 strictlyAfter(afterRootT, tb)) {
  279.             if (loopG == 0.0) {
  280.                 // ga == 0.0 and gb may or may not be 0.0
  281.                 // handle the root at ta first
  282.                 beforeRootT = loopT;
  283.                 beforeRootG = loopG;
  284.                 afterRootT = minTime(shiftedBy(beforeRootT, convergence), tb);
  285.                 afterRootG = g(interpolator.getInterpolatedState(afterRootT));
  286.             } else {
  287.                 // both non-zero, the usual case, use a root finder.
  288.                 try {
  289.                     // time zero for evaluating the function f. Needs to be final
  290.                     final AbsoluteDate fT0 = loopT;
  291.                     final UnivariateFunction f = dt -> {
  292.                         try {
  293.                             return g(interpolator.getInterpolatedState(fT0.shiftedBy(dt)));
  294.                         } catch (OrekitException oe) {
  295.                             throw new OrekitExceptionWrapper(oe);
  296.                         }
  297.                     };
  298.                     // tb as a double for use in f
  299.                     final double tbDouble = tb.durationFrom(fT0);
  300.                     if (forward) {
  301.                         final Interval interval =
  302.                                 solver.solveInterval(maxIterationCount, f, 0, tbDouble);
  303.                         beforeRootT = fT0.shiftedBy(interval.getLeftAbscissa());
  304.                         beforeRootG = interval.getLeftValue();
  305.                         afterRootT = fT0.shiftedBy(interval.getRightAbscissa());
  306.                         afterRootG = interval.getRightValue();
  307.                     } else {
  308.                         final Interval interval =
  309.                                 solver.solveInterval(maxIterationCount, f, tbDouble, 0);
  310.                         beforeRootT = fT0.shiftedBy(interval.getRightAbscissa());
  311.                         beforeRootG = interval.getRightValue();
  312.                         afterRootT = fT0.shiftedBy(interval.getLeftAbscissa());
  313.                         afterRootG = interval.getLeftValue();
  314.                     }
  315.                 } catch (OrekitExceptionWrapper oew) {
  316.                     throw oew.getException();
  317.                 }
  318.             }
  319.             // tolerance is set to less than 1 ulp
  320.             // assume tolerance is 1 ulp
  321.             if (beforeRootT.equals(afterRootT)) {
  322.                 afterRootT = nextAfter(afterRootT);
  323.                 afterRootG = g(interpolator.getInterpolatedState(afterRootT));
  324.             }
  325.             // check loop is making some progress
  326.             check((forward && afterRootT.compareTo(beforeRootT) > 0) ||
  327.                   (!forward && afterRootT.compareTo(beforeRootT) < 0));
  328.             // setup next iteration
  329.             loopT = afterRootT;
  330.             loopG = afterRootG;
  331.         }

  332.         // figure out the result of root finding, and return accordingly
  333.         if (afterRootG == 0.0 || afterRootG > 0.0 == g0Positive) {
  334.             // loop gave up and didn't find any crossing within this step
  335.             return false;
  336.         } else {
  337.             // real crossing
  338.             check(beforeRootT != null && !Double.isNaN(beforeRootG));
  339.             // variation direction, with respect to the integration direction
  340.             increasing = !g0Positive;
  341.             pendingEventTime = beforeRootT;
  342.             stopTime = beforeRootG == 0.0 ? beforeRootT : afterRootT;
  343.             pendingEvent = true;
  344.             afterEvent = afterRootT;
  345.             afterG = afterRootG;

  346.             // check increasing set correctly
  347.             check(afterG > 0 == increasing);
  348.             check(increasing == gb >= ga);

  349.             return true;
  350.         }

  351.     }

  352.     /**
  353.      * Get the next number after the given number in the current propagation direction.
  354.      *
  355.      * @param t input time
  356.      * @return t +/- 1 ulp depending on the direction.
  357.      */
  358.     private AbsoluteDate nextAfter(final AbsoluteDate t) {
  359.         return t.shiftedBy(forward ? +Precision.EPSILON : -Precision.EPSILON);
  360.     }

  361.     /** Get the occurrence time of the event triggered in the current
  362.      * step.
  363.      * @return occurrence time of the event triggered in the current
  364.      * step.
  365.      */
  366.     public AbsoluteDate getEventDate() {
  367.         return pendingEventTime;
  368.     }

  369.     /**
  370.      * Try to accept the current history up to the given time.
  371.      *
  372.      * <p> It is not necessary to call this method before calling {@link
  373.      * #doEvent(SpacecraftState)} with the same state. It is necessary to call this
  374.      * method before you call {@link #doEvent(SpacecraftState)} on some other event
  375.      * detector.
  376.      *
  377.      * @param state        to try to accept.
  378.      * @param interpolator to use to find the new root, if any.
  379.      * @return if the event detector has an event it has not detected before that is on or
  380.      * before the same time as {@code state}. In other words {@code false} means continue
  381.      * on while {@code true} means stop and handle my event first.
  382.      * @exception OrekitException if the g function throws one
  383.      */
  384.     public boolean tryAdvance(final SpacecraftState state,
  385.                               final OrekitStepInterpolator interpolator)
  386.         throws OrekitException {
  387.         // check this is only called before a pending event.
  388.         check(!(pendingEvent && strictlyAfter(pendingEventTime, state.getDate())));

  389.         final AbsoluteDate t = state.getDate();

  390.         // just found an event and we know the next time we want to search again
  391.         if (strictlyAfter(t, earliestTimeConsidered)) {
  392.             return false;
  393.         }

  394.         final double g = g(state);
  395.         final boolean positive = g > 0;

  396.         // check for new root, pendingEventTime may be null if there is not pending event
  397.         if ((g == 0.0 && t.equals(pendingEventTime)) || positive == g0Positive) {
  398.             // at a root we already found, or g function has expected sign
  399.             t0 = t;
  400.             g0 = g; // g0Positive is the same
  401.             return false;
  402.         } else {
  403.             // found a root we didn't expect -> find precise location
  404.             return findRoot(interpolator, t0, g0, t, g);
  405.         }
  406.     }

  407.     /**
  408.      * Notify the user's listener of the event. The event occurs wholly within this method
  409.      * call including a call to {@link EventDetector#resetState(SpacecraftState)}
  410.      * if necessary.
  411.      *
  412.      * @param state the state at the time of the event. This must be at the same time as
  413.      *              the current value of {@link #getEventDate()}.
  414.      * @return the user's requested action and the new state if the action is {@link
  415.      * org.orekit.propagation.events.handlers.EventHandler.Action#RESET_STATE Action.RESET_STATE}.
  416.      * Otherwise the new state is {@code state}. The stop time indicates what time propagation
  417.      * should stop if the action is {@link
  418.      * org.orekit.propagation.events.handlers.EventHandler.Action#STOP Action.STOP}.
  419.      * This guarantees the integration will stop on or after the root, so that integration
  420.      * may be restarted safely.
  421.      * @exception OrekitException if the event detector throws one
  422.      */
  423.     public EventOccurrence doEvent(final SpacecraftState state)
  424.         throws OrekitException {
  425.         // check event is pending and is at the same time
  426.         check(pendingEvent);
  427.         check(state.getDate().equals(this.pendingEventTime));

  428.         final EventHandler.Action action = detector.eventOccurred(state, increasing == forward);
  429.         final SpacecraftState newState;
  430.         if (action == EventHandler.Action.RESET_STATE) {
  431.             newState = detector.resetState(state);
  432.         } else {
  433.             newState = state;
  434.         }
  435.         // clear pending event
  436.         pendingEvent     = false;
  437.         pendingEventTime = null;
  438.         // setup for next search
  439.         earliestTimeConsidered = afterEvent;
  440.         t0 = afterEvent;
  441.         g0 = afterG;
  442.         g0Positive = increasing;
  443.         // check g0Positive set correctly
  444.         check(g0 == 0.0 || g0Positive == (g0 > 0));
  445.         return new EventOccurrence(action, newState, stopTime);
  446.     }

  447.     /**
  448.      * Shift a time value along the current integration direction: {@link #forward}.
  449.      *
  450.      * @param t     the time to shift.
  451.      * @param delta the amount to shift.
  452.      * @return t + delta if forward, else t - delta. If the result has to be rounded it
  453.      * will be rounded to be before the true value of t + delta.
  454.      */
  455.     private AbsoluteDate shiftedBy(final AbsoluteDate t, final double delta) {
  456.         if (forward) {
  457.             final AbsoluteDate ret = t.shiftedBy(delta);
  458.             if (ret.durationFrom(t) > delta) {
  459.                 return ret.shiftedBy(-Precision.EPSILON);
  460.             } else {
  461.                 return ret;
  462.             }
  463.         } else {
  464.             final AbsoluteDate ret = t.shiftedBy(-delta);
  465.             if (t.durationFrom(ret) > delta) {
  466.                 return ret.shiftedBy(+Precision.EPSILON);
  467.             } else {
  468.                 return ret;
  469.             }
  470.         }
  471.     }

  472.     /**
  473.      * Get the time that happens first along the current propagation direction: {@link
  474.      * #forward}.
  475.      *
  476.      * @param a first time
  477.      * @param b second time
  478.      * @return min(a, b) if forward, else max (a, b)
  479.      */
  480.     private AbsoluteDate minTime(final AbsoluteDate a, final AbsoluteDate b) {
  481.         return (forward ^ (a.compareTo(b) > 0)) ? a : b;
  482.     }

  483.     /**
  484.      * Check the ordering of two times.
  485.      *
  486.      * @param t1 the first time.
  487.      * @param t2 the second time.
  488.      * @return true if {@code t2} is strictly after {@code t1} in the propagation
  489.      * direction.
  490.      */
  491.     private boolean strictlyAfter(final AbsoluteDate t1, final AbsoluteDate t2) {
  492.         if (t1 == null || t2 == null) {
  493.             return false;
  494.         } else {
  495.             return forward ? t1.compareTo(t2) < 0 : t2.compareTo(t1) < 0;
  496.         }
  497.     }

  498.     /**
  499.      * Same as keyword assert, but throw a {@link MathRuntimeException}.
  500.      *
  501.      * @param condition to check
  502.      * @throws MathRuntimeException if {@code condition} is false.
  503.      */
  504.     private void check(final boolean condition) throws MathRuntimeException {
  505.         if (!condition) {
  506.             throw new OrekitInternalError(null);
  507.         }
  508.     }

  509.     /**
  510.      * Class to hold the data related to an event occurrence that is needed to decide how
  511.      * to modify integration.
  512.      */
  513.     public static class EventOccurrence {

  514.         /** User requested action. */
  515.         private final EventHandler.Action action;
  516.         /** New state for a reset action. */
  517.         private final SpacecraftState newState;
  518.         /** The time to stop propagation if the action is a stop event. */
  519.         private final AbsoluteDate stopDate;

  520.         /**
  521.          * Create a new occurrence of an event.
  522.          *
  523.          * @param action   the user requested action.
  524.          * @param newState for a reset event. Should be the current state unless the
  525.          *                 action is {@link Action#RESET_STATE}.
  526.          * @param stopDate to stop propagation if the action is {@link Action#STOP}. Used
  527.          *                 to move the stop time to just after the root.
  528.          */
  529.         EventOccurrence(final EventHandler.Action action,
  530.                         final SpacecraftState newState,
  531.                         final AbsoluteDate stopDate) {
  532.             this.action = action;
  533.             this.newState = newState;
  534.             this.stopDate = stopDate;
  535.         }

  536.         /**
  537.          * Get the user requested action.
  538.          *
  539.          * @return the action.
  540.          */
  541.         public EventHandler.Action getAction() {
  542.             return action;
  543.         }

  544.         /**
  545.          * Get the new state for a reset action.
  546.          *
  547.          * @return the new state.
  548.          */
  549.         public SpacecraftState getNewState() {
  550.             return newState;
  551.         }

  552.         /**
  553.          * Get the new time for a stop action.
  554.          *
  555.          * @return when to stop propagation.
  556.          */
  557.         public AbsoluteDate getStopDate() {
  558.             return stopDate;
  559.         }

  560.     }

  561. }