@Test
  public void testSpin() throws OrekitException {

    AbsoluteDate date =
        new AbsoluteDate(
            new DateComponents(1970, 01, 01),
            new TimeComponents(3, 25, 45.6789),
            TimeScalesFactory.getUTC());
    KeplerianOrbit orbit =
        new KeplerianOrbit(
            7178000.0,
            1.e-4,
            FastMath.toRadians(50.),
            FastMath.toRadians(10.),
            FastMath.toRadians(20.),
            FastMath.toRadians(30.),
            PositionAngle.MEAN,
            FramesFactory.getEME2000(),
            date,
            3.986004415e14);

    final AttitudeProvider law =
        new LofOffsetPointing(
            earthSpheric,
            new LofOffset(orbit.getFrame(), LOFType.VVLH, RotationOrder.XYX, 0.1, 0.2, 0.3),
            Vector3D.PLUS_K);

    Propagator propagator = new KeplerianPropagator(orbit, law);

    double h = 0.01;
    SpacecraftState sMinus = propagator.propagate(date.shiftedBy(-h));
    SpacecraftState s0 = propagator.propagate(date);
    SpacecraftState sPlus = propagator.propagate(date.shiftedBy(h));

    // check spin is consistent with attitude evolution
    double errorAngleMinus =
        Rotation.distance(
            sMinus.shiftedBy(h).getAttitude().getRotation(), s0.getAttitude().getRotation());
    double evolutionAngleMinus =
        Rotation.distance(sMinus.getAttitude().getRotation(), s0.getAttitude().getRotation());
    Assert.assertEquals(0.0, errorAngleMinus, 1.0e-6 * evolutionAngleMinus);
    double errorAnglePlus =
        Rotation.distance(
            s0.getAttitude().getRotation(), sPlus.shiftedBy(-h).getAttitude().getRotation());
    double evolutionAnglePlus =
        Rotation.distance(s0.getAttitude().getRotation(), sPlus.getAttitude().getRotation());
    Assert.assertEquals(0.0, errorAnglePlus, 1.0e-6 * evolutionAnglePlus);

    Vector3D spin0 = s0.getAttitude().getSpin();
    Vector3D reference =
        AngularCoordinates.estimateRate(
            sMinus.getAttitude().getRotation(), sPlus.getAttitude().getRotation(), 2 * h);
    Assert.assertTrue(spin0.getNorm() > 1.0e-3);
    Assert.assertEquals(0.0, spin0.subtract(reference).getNorm(), 1.0e-10);
  }
  @Test
  public void testResetState() throws OrekitException {
    final List<AbsoluteDate> reset = new ArrayList<AbsoluteDate>();
    DateDetector raw =
        new DateDetector(orbit.getDate().shiftedBy(3600.0))
            .withMaxCheck(1000.0)
            .withHandler(
                new EventHandler<DateDetector>() {
                  public SpacecraftState resetState(
                      DateDetector detector, SpacecraftState oldState) {
                    reset.add(oldState.getDate());
                    return oldState;
                  }

                  public Action eventOccurred(
                      SpacecraftState s, DateDetector detector, boolean increasing) {
                    return Action.RESET_STATE;
                  }
                });
    for (int i = 2; i < 10; ++i) {
      raw.addEventDate(orbit.getDate().shiftedBy(i * 3600.0));
    }
    EventEnablingPredicateFilter<DateDetector> filtered =
        new EventEnablingPredicateFilter<DateDetector>(
            raw,
            new EnablingPredicate<DateDetector>() {
              public boolean eventIsEnabled(
                  SpacecraftState state, DateDetector eventDetector, double g) {
                return state.getDate().durationFrom(orbit.getDate()) > 20000.0;
              }
            });
    Propagator propagator = new KeplerianPropagator(orbit);
    EventsLogger logger = new EventsLogger();
    propagator.addEventDetector(logger.monitorDetector(filtered));
    propagator.propagate(orbit.getDate().shiftedBy(Constants.JULIAN_DAY));
    List<LoggedEvent> events = logger.getLoggedEvents();
    Assert.assertEquals(4, events.size());
    Assert.assertEquals(
        6 * 3600, events.get(0).getState().getDate().durationFrom(orbit.getDate()), 1.0e-6);
    Assert.assertEquals(
        7 * 3600, events.get(1).getState().getDate().durationFrom(orbit.getDate()), 1.0e-6);
    Assert.assertEquals(
        8 * 3600, events.get(2).getState().getDate().durationFrom(orbit.getDate()), 1.0e-6);
    Assert.assertEquals(
        9 * 3600, events.get(3).getState().getDate().durationFrom(orbit.getDate()), 1.0e-6);
    Assert.assertEquals(4, reset.size());
    Assert.assertEquals(6 * 3600, reset.get(0).durationFrom(orbit.getDate()), 1.0e-6);
    Assert.assertEquals(7 * 3600, reset.get(1).durationFrom(orbit.getDate()), 1.0e-6);
    Assert.assertEquals(8 * 3600, reset.get(2).durationFrom(orbit.getDate()), 1.0e-6);
    Assert.assertEquals(9 * 3600, reset.get(3).durationFrom(orbit.getDate()), 1.0e-6);
  }
  @Test
  public void testFrance() throws OrekitException {

    final BodyShape earth =
        new OneAxisEllipsoid(
            Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
            Constants.WGS84_EARTH_FLATTENING,
            FramesFactory.getITRF(IERSConventions.IERS_2010, true));

    GeographicZoneDetector d =
        new GeographicZoneDetector(20.0, 1.e-3, earth, buildFrance(), FastMath.toRadians(0.5))
            .withHandler(new ContinueOnEvent<GeographicZoneDetector>());

    Assert.assertEquals(20.0, d.getMaxCheckInterval(), 1.0e-15);
    Assert.assertEquals(1.0e-3, d.getThreshold(), 1.0e-15);
    Assert.assertEquals(0.5, FastMath.toDegrees(d.getMargin()), 1.0e-15);
    Assert.assertEquals(AbstractDetector.DEFAULT_MAX_ITER, d.getMaxIterationCount());

    final TimeScale utc = TimeScalesFactory.getUTC();
    final Vector3D position = new Vector3D(-6142438.668, 3492467.56, -25767.257);
    final Vector3D velocity = new Vector3D(505.848, 942.781, 7435.922);
    final AbsoluteDate date = new AbsoluteDate(2003, 9, 16, utc);
    final Orbit orbit =
        new EquinoctialOrbit(
            new PVCoordinates(position, velocity),
            FramesFactory.getEME2000(),
            date,
            Constants.EIGEN5C_EARTH_MU);

    Propagator propagator =
        new EcksteinHechlerPropagator(
            orbit,
            Constants.EIGEN5C_EARTH_EQUATORIAL_RADIUS,
            Constants.EIGEN5C_EARTH_MU,
            Constants.EIGEN5C_EARTH_C20,
            Constants.EIGEN5C_EARTH_C30,
            Constants.EIGEN5C_EARTH_C40,
            Constants.EIGEN5C_EARTH_C50,
            Constants.EIGEN5C_EARTH_C60);

    EventsLogger logger = new EventsLogger();
    propagator.addEventDetector(logger.monitorDetector(d));

    propagator.propagate(date.shiftedBy(10 * Constants.JULIAN_DAY));
    Assert.assertEquals(26, logger.getLoggedEvents().size());
  }
  @Test
  public void testEphemerisDatesBackward() throws OrekitException {
    // setup
    TimeScale tai = TimeScalesFactory.getTAI();
    AbsoluteDate initialDate = new AbsoluteDate("2015-07-05", tai);
    AbsoluteDate startDate = new AbsoluteDate("2015-07-03", tai).shiftedBy(-0.1);
    AbsoluteDate endDate = new AbsoluteDate("2015-07-04", tai);
    Frame eci = FramesFactory.getGCRF();
    KeplerianOrbit orbit =
        new KeplerianOrbit(
            600e3 + Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
            0,
            0,
            0,
            0,
            0,
            PositionAngle.TRUE,
            eci,
            initialDate,
            mu);
    double[][] tol = NumericalPropagator.tolerances(1, orbit, OrbitType.CARTESIAN);
    Propagator prop =
        new NumericalPropagator(new DormandPrince853Integrator(0.1, 500, tol[0], tol[1]));
    prop.resetInitialState(new SpacecraftState(new CartesianOrbit(orbit)));

    // action
    prop.setEphemerisMode();
    prop.propagate(endDate, startDate);
    BoundedPropagator ephemeris = prop.getGeneratedEphemeris();

    // verify
    TimeStampedPVCoordinates actualPV = ephemeris.getPVCoordinates(startDate, eci);
    TimeStampedPVCoordinates expectedPV = orbit.getPVCoordinates(startDate, eci);
    MatcherAssert.assertThat(
        actualPV.getPosition(), OrekitMatchers.vectorCloseTo(expectedPV.getPosition(), 1.0));
    MatcherAssert.assertThat(
        actualPV.getVelocity(), OrekitMatchers.vectorCloseTo(expectedPV.getVelocity(), 1.0));
    MatcherAssert.assertThat(
        ephemeris.getMinDate().durationFrom(startDate), OrekitMatchers.closeTo(0, 0));
    MatcherAssert.assertThat(
        ephemeris.getMaxDate().durationFrom(endDate), OrekitMatchers.closeTo(0, 0));
    // test date
    AbsoluteDate date = endDate.shiftedBy(-0.11);
    Assert.assertEquals(ephemeris.propagate(date).getDate().durationFrom(date), 0, 0);
  }
  @Test
  public void testExceedHistoryBackward() throws OrekitException, IOException {
    final double period = 900.0;

    // the raw detector should trigger one event at each 900s period
    final DateDetector raw =
        new DateDetector(orbit.getDate().shiftedBy(+0.5 * period))
            .withMaxCheck(period / 3)
            .withHandler(new ContinueOnEvent<DateDetector>());
    for (int i = 0; i < 300; ++i) {
      raw.addEventDate(orbit.getDate().shiftedBy(-(i + 0.5) * period));
    }

    // in fact, we will filter out half of these events, so we get only one event every 2 periods
    final EventEnablingPredicateFilter<DateDetector> filtered =
        new EventEnablingPredicateFilter<DateDetector>(
            raw,
            new EnablingPredicate<DateDetector>() {
              public boolean eventIsEnabled(
                  SpacecraftState state, DateDetector eventDetector, double g) {
                double nbPeriod = orbit.getDate().durationFrom(state.getDate()) / period;
                return ((int) FastMath.floor(nbPeriod)) % 2 == 1;
              }
            });
    Propagator propagator = new KeplerianPropagator(orbit);
    EventsLogger logger = new EventsLogger();
    propagator.addEventDetector(logger.monitorDetector(filtered));
    propagator.propagate(orbit.getDate().shiftedBy(-301 * period));
    List<LoggedEvent> events = logger.getLoggedEvents();

    // 300 periods, 150 events as half of them are filtered out
    Assert.assertEquals(150, events.size());

    // as we have encountered a lot of enabling status changes, we exceeded the internal history
    // if we try to display again the filtered g function for dates far in the future,
    // we will not see the zero crossings anymore, they have been lost
    propagator.clearEventsDetectors();
    for (double dt = -5000.0; dt > -10000.0; dt -= 3.0) {
      double filteredG = filtered.g(propagator.propagate(orbit.getDate().shiftedBy(dt)));
      Assert.assertTrue(filteredG < 0.0);
    }

    // on the other hand, if we try to display again the filtered g function for future dates
    // that are still inside the history, we still see the zero crossings
    for (double dt = -195400.0; dt > -196200.0; dt -= 3.0) {
      double filteredG = filtered.g(propagator.propagate(orbit.getDate().shiftedBy(dt)));
      if (dt < -195750) {
        Assert.assertTrue(filteredG < 0.0);
      } else {
        Assert.assertTrue(filteredG > 0.0);
      }
    }
  }
  private void doElevationTest(
      final double minElevation,
      final AbsoluteDate start,
      final AbsoluteDate end,
      final int expectedEvents,
      final boolean sameSign)
      throws OrekitException {

    final ElevationExtremumDetector raw =
        new ElevationExtremumDetector(0.001, 1.e-6, new TopocentricFrame(earth, gp, "test"))
            .withHandler(new ContinueOnEvent<ElevationExtremumDetector>());
    final EventEnablingPredicateFilter<ElevationExtremumDetector> aboveGroundElevationDetector =
        new EventEnablingPredicateFilter<ElevationExtremumDetector>(
                raw,
                new EnablingPredicate<ElevationExtremumDetector>() {
                  public boolean eventIsEnabled(
                      final SpacecraftState state,
                      final ElevationExtremumDetector eventDetector,
                      final double g)
                      throws OrekitException {
                    return eventDetector.getElevation(state) > minElevation;
                  }
                })
            .withMaxCheck(60.0);

    Assert.assertEquals(0.001, raw.getMaxCheckInterval(), 1.0e-15);
    Assert.assertEquals(60.0, aboveGroundElevationDetector.getMaxCheckInterval(), 1.0e-15);
    Assert.assertEquals(1.0e-6, aboveGroundElevationDetector.getThreshold(), 1.0e-15);
    Assert.assertEquals(
        AbstractDetector.DEFAULT_MAX_ITER, aboveGroundElevationDetector.getMaxIterationCount());

    Propagator propagator =
        new EcksteinHechlerPropagator(
            orbit,
            Constants.EIGEN5C_EARTH_EQUATORIAL_RADIUS,
            Constants.EIGEN5C_EARTH_MU,
            Constants.EIGEN5C_EARTH_C20,
            Constants.EIGEN5C_EARTH_C30,
            Constants.EIGEN5C_EARTH_C40,
            Constants.EIGEN5C_EARTH_C50,
            Constants.EIGEN5C_EARTH_C60);

    EventsLogger logger = new EventsLogger();
    propagator.addEventDetector(logger.monitorDetector(aboveGroundElevationDetector));

    propagator.propagate(start, end);
    for (LoggedEvent e : logger.getLoggedEvents()) {
      final double eMinus = raw.getElevation(e.getState().shiftedBy(-10.0));
      final double e0 = raw.getElevation(e.getState());
      final double ePlus = raw.getElevation(e.getState().shiftedBy(+10.0));
      Assert.assertTrue(e0 > eMinus);
      Assert.assertTrue(e0 > ePlus);
      Assert.assertTrue(e0 > minElevation);
    }
    Assert.assertEquals(expectedEvents, logger.getLoggedEvents().size());

    propagator.clearEventsDetectors();
    double g1Raw = raw.g(propagator.propagate(orbit.getDate().shiftedBy(18540.0)));
    double g2Raw = raw.g(propagator.propagate(orbit.getDate().shiftedBy(18624.0)));
    double g1 =
        aboveGroundElevationDetector.g(propagator.propagate(orbit.getDate().shiftedBy(18540.0)));
    double g2 =
        aboveGroundElevationDetector.g(propagator.propagate(orbit.getDate().shiftedBy(18624.0)));
    Assert.assertTrue(g1Raw > 0);
    Assert.assertTrue(g2Raw < 0);
    if (sameSign) {
      Assert.assertTrue(g1 > 0);
      Assert.assertTrue(g2 < 0);
    } else {
      Assert.assertTrue(g1 < 0);
      Assert.assertTrue(g2 > 0);
    }
  }
  protected void checkFit(
      final TLE tle,
      final double duration,
      final double stepSize,
      final double threshold,
      final boolean positionOnly,
      final boolean withBStar,
      final double expectedRMS)
      throws OrekitException {

    Propagator p = TLEPropagator.selectExtrapolator(tle);
    List<SpacecraftState> sample = new ArrayList<SpacecraftState>();
    for (double dt = 0; dt < duration; dt += stepSize) {
      sample.add(p.propagate(tle.getDate().shiftedBy(dt)));
    }

    TLEPropagatorBuilder builder =
        new TLEPropagatorBuilder(
            tle.getSatelliteNumber(),
            tle.getClassification(),
            tle.getLaunchYear(),
            tle.getLaunchNumber(),
            tle.getLaunchPiece(),
            tle.getElementNumber(),
            tle.getRevolutionNumberAtEpoch());

    FiniteDifferencePropagatorConverter fitter =
        new FiniteDifferencePropagatorConverter(builder, threshold, 1000);

    if (withBStar) {
      fitter.convert(sample, positionOnly, TLEPropagatorBuilder.B_STAR);
    } else {
      fitter.convert(sample, positionOnly);
    }

    TLEPropagator prop = (TLEPropagator) fitter.getAdaptedPropagator();
    TLE fitted = prop.getTLE();

    Assert.assertEquals(expectedRMS, fitter.getRMS(), 0.001 * expectedRMS);

    Assert.assertEquals(tle.getSatelliteNumber(), fitted.getSatelliteNumber());
    Assert.assertEquals(tle.getClassification(), fitted.getClassification());
    Assert.assertEquals(tle.getLaunchYear(), fitted.getLaunchYear());
    Assert.assertEquals(tle.getLaunchNumber(), fitted.getLaunchNumber());
    Assert.assertEquals(tle.getLaunchPiece(), fitted.getLaunchPiece());
    Assert.assertEquals(tle.getElementNumber(), fitted.getElementNumber());
    Assert.assertEquals(tle.getRevolutionNumberAtEpoch(), fitted.getRevolutionNumberAtEpoch());

    final double eps = 1.0e-5;
    Assert.assertEquals(tle.getMeanMotion(), fitted.getMeanMotion(), eps * tle.getMeanMotion());
    Assert.assertEquals(tle.getE(), fitted.getE(), eps * tle.getE());
    Assert.assertEquals(tle.getI(), fitted.getI(), eps * tle.getI());
    Assert.assertEquals(
        tle.getPerigeeArgument(), fitted.getPerigeeArgument(), eps * tle.getPerigeeArgument());
    Assert.assertEquals(tle.getRaan(), fitted.getRaan(), eps * tle.getRaan());
    Assert.assertEquals(tle.getMeanAnomaly(), fitted.getMeanAnomaly(), eps * tle.getMeanAnomaly());

    if (withBStar) {
      Assert.assertEquals(tle.getBStar(), fitted.getBStar(), eps * tle.getBStar());
    }
  }