JPLCelestialBody.java

/* Copyright 2002-2024 CS GROUP
 * 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.bodies;

import java.io.Serializable;

import org.hipparchus.CalculusFieldElement;
import org.hipparchus.Field;
import org.hipparchus.geometry.euclidean.threed.FieldRotation;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.hipparchus.geometry.euclidean.threed.Rotation;
import org.hipparchus.geometry.euclidean.threed.RotationConvention;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.Precision;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.bodies.JPLEphemeridesLoader.EphemerisType;
import org.orekit.data.DataContext;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitInternalError;
import org.orekit.frames.FieldStaticTransform;
import org.orekit.frames.FieldTransform;
import org.orekit.frames.Frame;
import org.orekit.frames.StaticTransform;
import org.orekit.frames.Transform;
import org.orekit.frames.TransformProvider;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.FieldAbsoluteDate;
import org.orekit.utils.FieldPVCoordinates;
import org.orekit.utils.PVCoordinates;
import org.orekit.utils.TimeStampedFieldPVCoordinates;
import org.orekit.utils.TimeStampedPVCoordinates;

/** Implementation of the {@link CelestialBody} interface using JPL or INPOP ephemerides.
 * @author Luc Maisonobe
 */
class JPLCelestialBody implements CelestialBody {

    /** Serializable UID. */
    private static final long serialVersionUID = 3809787672779740923L;

    /** Name of the body. */
    private final String name;

    /** Regular expression for supported files names. */
    private final String supportedNames;

    /** Ephemeris type to generate. */
    private final JPLEphemeridesLoader.EphemerisType generateType;

    /** Raw position-velocity provider. */
    private final transient JPLEphemeridesLoader.RawPVProvider rawPVProvider;

    /** Attraction coefficient of the body (m³/s²). */
    private final double gm;

    /** Scaling factor for position-velocity. */
    private final double scale;

    /** IAU pole. */
    private final IAUPole iauPole;

    /** Inertially oriented, body-centered frame. */
    private final Frame inertialFrame;

    /** Body oriented, body-centered frame. */
    private final Frame bodyFrame;

    /** Build an instance and the underlying frame.
     * @param name name of the body
     * @param supportedNames regular expression for supported files names
     * @param generateType ephemeris type to generate
     * @param rawPVProvider raw position-velocity provider
     * @param gm attraction coefficient (in m³/s²)
     * @param scale scaling factor for position-velocity
     * @param iauPole IAU pole implementation
     * @param definingFrameAlignedWithICRF frame in which celestial body coordinates are defined,
     * this frame <strong>must</strong> be aligned with ICRF
     * @param inertialFrameName name to use for inertial frame (if null a default name will be built)
     * @param bodyOrientedFrameName name to use for body-oriented frame (if null a default name will be built)
     */
    JPLCelestialBody(final String name, final String supportedNames,
                     final JPLEphemeridesLoader.EphemerisType generateType,
                     final JPLEphemeridesLoader.RawPVProvider rawPVProvider,
                     final double gm, final double scale,
                     final IAUPole iauPole, final Frame definingFrameAlignedWithICRF,
                     final String inertialFrameName, final String bodyOrientedFrameName) {
        this.name           = name;
        this.gm             = gm;
        this.scale          = scale;
        this.supportedNames = supportedNames;
        this.generateType   = generateType;
        this.rawPVProvider  = rawPVProvider;
        this.iauPole        = iauPole;
        this.inertialFrame  = new InertiallyOriented(definingFrameAlignedWithICRF, inertialFrameName);
        this.bodyFrame      = new BodyOriented(bodyOrientedFrameName);
    }

    /** {@inheritDoc} */
    public TimeStampedPVCoordinates getPVCoordinates(final AbsoluteDate date, final Frame frame) {

        // apply the scale factor to raw position-velocity
        final PVCoordinates rawPV    = rawPVProvider.getRawPV(date);
        final TimeStampedPVCoordinates scaledPV = new TimeStampedPVCoordinates(date, scale, rawPV);

        // the raw PV are relative to the parent of the body centered inertially oriented frame
        final Transform transform = getInertiallyOrientedFrame().getParent().getTransformTo(frame, date);

        // convert to requested frame
        return transform.transformPVCoordinates(scaledPV);

    }

    /** Get the {@link FieldPVCoordinates} of the body in the selected frame.
     * @param date current date
     * @param frame the frame where to define the position
     * @param <T> type of the field elements
     * @return time-stamped position/velocity of the body (m and m/s)
     */
    public <T extends CalculusFieldElement<T>> TimeStampedFieldPVCoordinates<T> getPVCoordinates(final FieldAbsoluteDate<T> date,
                                                                                                 final Frame frame) {

        // apply the scale factor to raw position-velocity
        final FieldPVCoordinates<T>            rawPV    = rawPVProvider.getRawPV(date);
        final TimeStampedFieldPVCoordinates<T> scaledPV = new TimeStampedFieldPVCoordinates<>(date, scale, rawPV);

        // the raw PV are relative to the parent of the body centered inertially oriented frame
        final FieldTransform<T> transform = getInertiallyOrientedFrame().getParent().getTransformTo(frame, date);

        // convert to requested frame
        return transform.transformPVCoordinates(scaledPV);

    }

    /** {@inheritDoc} */
    @Override
    public Vector3D getPosition(final AbsoluteDate date, final Frame frame) {

        // apply the scale factor to raw position
        final Vector3D rawPosition    = rawPVProvider.getRawPosition(date);
        final Vector3D scaledPosition = rawPosition.scalarMultiply(scale);

        // the raw position is relative to the parent of the body centered inertially oriented frame
        final StaticTransform transform = getInertiallyOrientedFrame().getParent().getStaticTransformTo(frame, date);

        // convert to requested frame
        return transform.transformPosition(scaledPosition);
    }

    /** {@inheritDoc} */
    @Override
    public <T extends CalculusFieldElement<T>> FieldVector3D<T> getPosition(final FieldAbsoluteDate<T> date, final Frame frame) {

        // apply the scale factor to raw position
        final FieldVector3D<T> rawPosition     = rawPVProvider.getRawPosition(date);
        final FieldVector3D<T> scaledPosition  = rawPosition.scalarMultiply(scale);

        // the raw position is relative to the parent of the body centered inertially oriented frame
        final FieldStaticTransform<T> transform = getInertiallyOrientedFrame().getParent().getStaticTransformTo(frame, date);

        // convert to requested frame
        return transform.transformPosition(scaledPosition);
    }


    /** Replace the instance with a data transfer object for serialization.
     * <p>
     * This intermediate class serializes the files supported names, the ephemeris type
     * and the body name.
     * </p>
     * @return data transfer object that will be serialized
     */
    @DefaultDataContext
    private Object writeReplace() {
        return new DTOCelestialBody(supportedNames, generateType, name);
    }

    /** {@inheritDoc} */
    public String getName() {
        return name;
    }

    /** {@inheritDoc} */
    public double getGM() {
        return gm;
    }

    /** {@inheritDoc} */
    public Frame getInertiallyOrientedFrame() {
        return inertialFrame;
    }

    /** {@inheritDoc} */
    public Frame getBodyOrientedFrame() {
        return bodyFrame;
    }

    /** Inertially oriented body centered frame. */
    private class InertiallyOriented extends Frame {

        /** Serializable UID. */
        private static final long serialVersionUID = -8849993808761896559L;

        /** Suffix for inertial frame name. */
        private static final String INERTIAL_FRAME_SUFFIX = "/inertial";

        /** Simple constructor.
         * @param definingFrame frame in which celestial body coordinates are defined
         * @param frameName name to use (if null a default name will be built)
         */
        InertiallyOriented(final Frame definingFrame, final String frameName) {
            super(definingFrame, new TransformProvider() {

                /** Serializable UID. */
                private static final long serialVersionUID = -8610328386110652400L;

                /** {@inheritDoc} */
                public Transform getTransform(final AbsoluteDate date) {

                    // compute translation from parent frame to self
                    final PVCoordinates pv = getPVCoordinates(date, definingFrame);
                    final Transform translation = new Transform(date, pv.negate());

                    // compute rotation from ICRF frame to self,
                    // as per the "Report of the IAU/IAG Working Group on Cartographic
                    // Coordinates and Rotational Elements of the Planets and Satellites"
                    // These definitions are common for all recent versions of this report
                    // published every three years, the precise values of pole direction
                    // and W angle coefficients may vary from publication year as models are
                    // adjusted. These coefficients are not in this class, they are in the
                    // specialized classes that do implement the getPole and getPrimeMeridianAngle
                    // methods
                    final Vector3D pole  = iauPole.getPole(date);
                    final Vector3D qNode = iauPole.getNode(date);
                    final Transform rotation =
                                    new Transform(date, new Rotation(pole, qNode, Vector3D.PLUS_K, Vector3D.PLUS_I));

                    // update transform from parent to self
                    return new Transform(date, translation, rotation);

                }

                @Override
                public StaticTransform getStaticTransform(final AbsoluteDate date) {
                    // compute translation from parent frame to self
                    final PVCoordinates pv = getPVCoordinates(date, definingFrame);

                    // compute rotation from ICRF frame to self,
                    // as per the "Report of the IAU/IAG Working Group on Cartographic
                    // Coordinates and Rotational Elements of the Planets and Satellites"
                    // These definitions are common for all recent versions of this report
                    // published every three years, the precise values of pole direction
                    // and W angle coefficients may vary from publication year as models are
                    // adjusted. These coefficients are not in this class, they are in the
                    // specialized classes that do implement the getPole and getPrimeMeridianAngle
                    // methods
                    final Vector3D pole  = iauPole.getPole(date);
                    final Vector3D qNode = iauPole.getNode(date);
                    final Rotation rotation =
                                    new Rotation(pole, qNode, Vector3D.PLUS_K, Vector3D.PLUS_I);

                    // update transform from parent to self
                    return StaticTransform.of(date, pv.getPosition().negate(), rotation);
                }

                /** {@inheritDoc} */
                public <T extends CalculusFieldElement<T>> FieldTransform<T> getTransform(final FieldAbsoluteDate<T> date) {

                    // compute translation from parent frame to self
                    final FieldPVCoordinates<T> pv = getPVCoordinates(date, definingFrame);
                    final FieldTransform<T> translation = new FieldTransform<>(date, pv.negate());

                    // compute rotation from ICRF frame to self,
                    // as per the "Report of the IAU/IAG Working Group on Cartographic
                    // Coordinates and Rotational Elements of the Planets and Satellites"
                    // These definitions are common for all recent versions of this report
                    // published every three years, the precise values of pole direction
                    // and W angle coefficients may vary from publication year as models are
                    // adjusted. These coefficients are not in this class, they are in the
                    // specialized classes that do implement the getPole and getPrimeMeridianAngle
                    // methods
                    final FieldVector3D<T> pole  = iauPole.getPole(date);
                    FieldVector3D<T> qNode = FieldVector3D.crossProduct(Vector3D.PLUS_K, pole);
                    if (qNode.getNormSq().getReal() < Precision.SAFE_MIN) {
                        qNode = FieldVector3D.getPlusI(date.getField());
                    }
                    final FieldTransform<T> rotation =
                                    new FieldTransform<>(date,
                                                    new FieldRotation<>(pole,
                                                                    qNode,
                                                                    FieldVector3D.getPlusK(date.getField()),
                                                                    FieldVector3D.getPlusI(date.getField())));

                    // update transform from parent to self
                    return new FieldTransform<>(date, translation, rotation);

                }

                @Override
                public <T extends CalculusFieldElement<T>> FieldStaticTransform<T> getStaticTransform(final FieldAbsoluteDate<T> date) {
                    // field
                    final Field<T> field = date.getField();
                    // compute translation from parent frame to self
                    final FieldPVCoordinates<T> pv = getPVCoordinates(date, definingFrame);

                    // compute rotation from ICRF frame to self,
                    // as per the "Report of the IAU/IAG Working Group on Cartographic
                    // Coordinates and Rotational Elements of the Planets and Satellites"
                    // These definitions are common for all recent versions of this report
                    // published every three years, the precise values of pole direction
                    // and W angle coefficients may vary from publication year as models are
                    // adjusted. These coefficients are not in this class, they are in the
                    // specialized classes that do implement the getPole and getPrimeMeridianAngle
                    // methods
                    final FieldVector3D<T> pole  = iauPole.getPole(date);
                    final FieldVector3D<T> qNode = iauPole.getNode(date);
                    final FieldRotation<T> rotation =
                                    new FieldRotation<>(pole, qNode, FieldVector3D.getPlusK(field), FieldVector3D.getPlusI(field));

                    // update transform from parent to self
                    return FieldStaticTransform.of(date, pv.getPosition().negate(), rotation);
                }

            }, frameName == null ? name + INERTIAL_FRAME_SUFFIX : frameName, true);
        }

        /** Replace the instance with a data transfer object for serialization.
         * <p>
         * This intermediate class serializes the files supported names, the ephemeris type
         * and the body name.
         * </p>
         * @return data transfer object that will be serialized
         */
        @DefaultDataContext
        private Object writeReplace() {
            return new DTOInertialFrame(supportedNames, generateType, name);
        }

    }

    /** Body oriented body centered frame. */
    private class BodyOriented extends Frame {

        /** Serializable UID. */
        private static final long serialVersionUID = 20170109L;

        /** Suffix for body frame name. */
        private static final String BODY_FRAME_SUFFIX = "/rotating";

        /** Simple constructor.
         * @param frameName name to use (if null a default name will be built)
         */
        BodyOriented(final String frameName) {
            super(inertialFrame, new TransformProvider() {

                /** Serializable UID. */
                private static final long serialVersionUID = 20170109L;

                /** {@inheritDoc} */
                public Transform getTransform(final AbsoluteDate date) {
                    final double dt = 10.0;
                    final double w0 = iauPole.getPrimeMeridianAngle(date);
                    final double w1 = iauPole.getPrimeMeridianAngle(date.shiftedBy(dt));
                    return new Transform(date,
                                         new Rotation(Vector3D.PLUS_K, w0, RotationConvention.FRAME_TRANSFORM),
                                         new Vector3D((w1 - w0) / dt, Vector3D.PLUS_K));
                }

                /** {@inheritDoc} */
                public <T extends CalculusFieldElement<T>> FieldTransform<T> getTransform(final FieldAbsoluteDate<T> date) {
                    final double dt = 10.0;
                    final T w0 = iauPole.getPrimeMeridianAngle(date);
                    final T w1 = iauPole.getPrimeMeridianAngle(date.shiftedBy(dt));
                    return new FieldTransform<>(date,
                                    new FieldRotation<>(FieldVector3D.getPlusK(date.getField()), w0,
                                                    RotationConvention.FRAME_TRANSFORM),
                                    new FieldVector3D<>(w1.subtract(w0).divide(dt), Vector3D.PLUS_K));
                }

            }, frameName == null ? name + BODY_FRAME_SUFFIX : frameName, false);
        }

        /** Replace the instance with a data transfer object for serialization.
         * <p>
         * This intermediate class serializes the files supported names, the ephemeris type
         * and the body name.
         * </p>
         * @return data transfer object that will be serialized
         */
        @DefaultDataContext
        private Object writeReplace() {
            return new DTOBodyFrame(supportedNames, generateType, name);
        }

    }

    /** Internal class used only for serialization. */
    @DefaultDataContext
    private abstract static class DataTransferObject implements Serializable {

        /** Serializable UID. */
        private static final long serialVersionUID = 674742836536072422L;

        /** Regular expression for supported files names. */
        private final String supportedNames;

        /** Ephemeris type to generate. */
        private final EphemerisType generateType;

        /** Name of the body. */
        private final String name;

        /** Simple constructor.
         * @param supportedNames regular expression for supported files names
         * @param generateType ephemeris type to generate
         * @param name name of the body
         */
        DataTransferObject(final String supportedNames, final EphemerisType generateType, final String name) {
            this.supportedNames = supportedNames;
            this.generateType   = generateType;
            this.name           = name;
        }

        /** Get the body associated with the serialized data.
         * @return body associated with the serialized data
         */
        protected JPLCelestialBody getBody() {

            try {
                // first try to use the factory, in order to avoid building a new instance
                // each time we deserialize and have the object properly cached
                final CelestialBody factoryProvided =
                                DataContext.getDefault().getCelestialBodies().getBody(name);
                if (factoryProvided instanceof JPLCelestialBody) {
                    final JPLCelestialBody jplBody = (JPLCelestialBody) factoryProvided;
                    if (supportedNames.equals(jplBody.supportedNames) && generateType == jplBody.generateType) {
                        // the factory created exactly the object we needed, just return it
                        return jplBody;
                    }
                }

                // the factory does not return the object we want
                // we create a new one from scratch and don't cache it
                return (JPLCelestialBody) new JPLEphemeridesLoader(supportedNames, generateType).loadCelestialBody(name);

            } catch (OrekitException oe) {
                throw new OrekitInternalError(oe);
            }

        }

    }

    /** Specialization of the data transfer object for complete celestial body serialization. */
    @DefaultDataContext
    private static class DTOCelestialBody extends DataTransferObject {

        /** Serializable UID. */
        private static final long serialVersionUID = -8287341529741045958L;

        /** Simple constructor.
         * @param supportedNames regular expression for supported files names
         * @param generateType ephemeris type to generate
         * @param name name of the body
         */
        DTOCelestialBody(final String supportedNames, final EphemerisType generateType, final String name) {
            super(supportedNames, generateType, name);
        }

        /** Replace the deserialized data transfer object with a {@link JPLCelestialBody}.
         * @return replacement {@link JPLCelestialBody}
         */
        private Object readResolve() {
            return getBody();
        }

    }

    /** Specialization of the data transfer object for inertially oriented frame serialization. */
    @DefaultDataContext
    private static class DTOInertialFrame extends DataTransferObject {

        /** Serializable UID. */
        private static final long serialVersionUID = 7915071664444154948L;

        /** Simple constructor.
         * @param supportedNames regular expression for supported files names
         * @param generateType ephemeris type to generate
         * @param name name of the body
         */
        DTOInertialFrame(final String supportedNames, final EphemerisType generateType, final String name) {
            super(supportedNames, generateType, name);
        }

        /** Replace the deserialized data transfer object with a {@link Frame}.
         * @return replacement {@link Frame}
         */
        private Object readResolve() {
            return getBody().inertialFrame;
        }

    }

    /** Specialization of the data transfer object for body oriented frame serialization. */
    @DefaultDataContext
    private static class DTOBodyFrame extends DataTransferObject {

        /** Serializable UID. */
        private static final long serialVersionUID = -3194195019557081000L;

        /** Simple constructor.
         * @param supportedNames regular expression for supported files names
         * @param generateType ephemeris type to generate
         * @param name name of the body
         */
        DTOBodyFrame(final String supportedNames, final EphemerisType generateType, final String name) {
            super(supportedNames, generateType, name);
        }

        /** Replace the deserialized data transfer object with a {@link Frame}.
         * @return replacement {@link Frame}
         */
        private Object readResolve() {
            return getBody().bodyFrame;
        }

    }

}