WaypointPVBuilder.java

  1. /* Copyright 2002-2025 Joseph Reed
  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.  * Joseph Reed 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.utils;

  18. import java.util.Map.Entry;
  19. import java.util.TreeMap;

  20. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  21. import org.hipparchus.geometry.spherical.twod.Circle;
  22. import org.hipparchus.geometry.spherical.twod.S2Point;
  23. import org.hipparchus.util.FastMath;
  24. import org.orekit.bodies.GeodeticPoint;
  25. import org.orekit.bodies.LoxodromeArc;
  26. import org.orekit.bodies.OneAxisEllipsoid;
  27. import org.orekit.frames.Frame;
  28. import org.orekit.frames.TopocentricFrame;
  29. import org.orekit.time.AbsoluteDate;
  30. import org.orekit.time.TimeOffset;

  31. /** Builder class, enabling incremental building of an {@link PVCoordinatesProvider}
  32.  * instance using waypoints defined on an ellipsoid.
  33.  * <p>
  34.  * Given a series of waypoints ({@code (date, point)} tuples),
  35.  * build a {@link PVCoordinatesProvider} representing the path.
  36.  * The static methods provide implementations for the most common path definitions
  37.  * (cartesian, great-circle, loxodrome). If these methods are insufficient,
  38.  * the public constructor provides a way to customize the path definition.
  39.  * </p>
  40.  * <p>
  41.  * This class connects the path segments using the {@link AggregatedPVCoordinatesProvider}.
  42.  * As such, no effort is made to smooth the velocity between segments.
  43.  * While position is unaffected, the velocity may be discontinuous between adjacent time points.
  44.  * Thus, care should be taken when modeling paths with abrupt direction changes
  45.  * (e.g. fast-moving aircraft); understand how the {@link PVCoordinatesProvider}
  46.  * will be used in the particular application.
  47.  * </p>
  48.  * @author Joe Reed
  49.  * @since 11.3
  50.  */
  51. public class WaypointPVBuilder {

  52.     /** Factory used to create intermediate pv providers between waypoints. */
  53.     private final InterpolationFactory factory;

  54.     /** Central body, on which the waypoints are defined. */
  55.     private final OneAxisEllipsoid body;

  56.     /** Set of waypoints, indexed by time. */
  57.     private final TreeMap<AbsoluteDate, GeodeticPoint> waypoints;

  58.     /** Whether the resulting provider should be invalid or constant prior to the first waypoint. */
  59.     private boolean invalidBefore;

  60.     /** Whether the resulting provider should be invalid or constant after to the last waypoint. */
  61.     private boolean invalidAfter;

  62.     /** Create a new instance.
  63.      * @param factory The factory used to create the intermediate coordinate providers between waypoints.
  64.      * @param body The central body, on which the way points are defined.
  65.      */
  66.     public WaypointPVBuilder(final InterpolationFactory factory, final OneAxisEllipsoid body) {
  67.         this.factory       = factory;
  68.         this.body          = body;
  69.         this.waypoints     = new TreeMap<>();
  70.         this.invalidBefore = true;
  71.         this.invalidAfter  = true;
  72.     }

  73.     /** Construct a waypoint builder interpolating points using a linear cartesian interpolation.
  74.      *
  75.      * @param body the reference ellipsoid on which the waypoints are defined.
  76.      * @return the waypoint builder
  77.      */
  78.     public static WaypointPVBuilder cartesianBuilder(final OneAxisEllipsoid body) {
  79.         return new WaypointPVBuilder(CartesianWaypointPVProv::new, body);
  80.     }

  81.     /** Construct a waypoint builder interpolating points using a loxodrome (or Rhumbline).
  82.      *
  83.      * @param body the reference ellipsoid on which the waypoints are defined.
  84.      * @return the waypoint builder
  85.      */
  86.     public static WaypointPVBuilder loxodromeBuilder(final OneAxisEllipsoid body) {
  87.         return new WaypointPVBuilder(LoxodromeWaypointPVProv::new, body);
  88.     }

  89.     /** Construct a waypoint builder interpolating points using a great-circle.
  90.      * <p>
  91.      * The altitude of the intermediate points is linearly interpolated from the bounding waypoints.
  92.      * Extrapolating before the first waypoint or after the last waypoint may result in undefined altitudes.
  93.      * </p>
  94.      * @param body the reference ellipsoid on which the waypoints are defined.
  95.      * @return the waypoint builder
  96.      */
  97.     public static WaypointPVBuilder greatCircleBuilder(final OneAxisEllipsoid body) {
  98.         return new WaypointPVBuilder(GreatCircleWaypointPVProv::new, body);
  99.     }

  100.     /** Add a waypoint.
  101.      *
  102.      * @param point the waypoint location
  103.      * @param date the waypoint time
  104.      * @return this instance
  105.      */
  106.     public WaypointPVBuilder addWaypoint(final GeodeticPoint point, final AbsoluteDate date) {
  107.         waypoints.put(date, point);
  108.         return this;
  109.     }

  110.     /** Indicate the resulting {@link PVCoordinatesProvider} should be invalid before the first waypoint.
  111.      *
  112.      * @return this instance
  113.      */
  114.     public WaypointPVBuilder invalidBefore() {
  115.         invalidBefore = true;
  116.         return this;
  117.     }

  118.     /** Indicate the resulting {@link PVCoordinatesProvider} provide
  119.      * a constant location of the first waypoint prior to the first time.
  120.      *
  121.      * @return this instance
  122.      */
  123.     public WaypointPVBuilder constantBefore() {
  124.         invalidBefore = false;
  125.         return this;
  126.     }

  127.     /** Indicate the resulting {@link PVCoordinatesProvider} should be invalid after the last waypoint.
  128.      *
  129.      * @return this instance
  130.      */
  131.     public WaypointPVBuilder invalidAfter() {
  132.         invalidAfter = true;
  133.         return this;
  134.     }

  135.     /** Indicate the resulting {@link PVCoordinatesProvider} provide
  136.      * a constant location of the last waypoint after to the last time.
  137.      *
  138.      * @return this instance
  139.      */
  140.     public WaypointPVBuilder constantAfter() {
  141.         invalidAfter = false;
  142.         return this;
  143.     }

  144.     /** Build a {@link PVCoordinatesProvider} from the waypoints added to this builder.
  145.      *
  146.      * @return the coordinates provider instance.
  147.      */
  148.     public PVCoordinatesProvider build() {
  149.         final PVCoordinatesProvider initialProvider = createInitial(waypoints.firstEntry().getValue());
  150.         final AggregatedPVCoordinatesProvider.Builder builder = new AggregatedPVCoordinatesProvider.Builder(initialProvider);

  151.         Entry<AbsoluteDate, GeodeticPoint> previousEntry = null;
  152.         for (final Entry<AbsoluteDate, GeodeticPoint> entry: waypoints.entrySet()) {
  153.             if (previousEntry != null) {
  154.                 builder.addPVProviderAfter(previousEntry.getKey(),
  155.                                            factory.create(previousEntry.getKey(),
  156.                                                           previousEntry.getValue(),
  157.                                                           entry.getKey(),
  158.                                                           entry.getValue(),
  159.                                                           body),
  160.                                            true);
  161.             }
  162.             previousEntry = entry;
  163.         }
  164.         // add the point so we're valid at the final waypoint
  165.         builder.addPVProviderAfter(previousEntry.getKey(),
  166.                                    new ConstantPVCoordinatesProvider(previousEntry.getValue(), body),
  167.                                    true);
  168.         // add the final provider after the final waypoint
  169.         builder.addPVProviderAfter(previousEntry.getKey().shiftedBy(TimeOffset.ATTOSECOND),
  170.                                    createFinal(previousEntry.getValue()),
  171.                                    true);

  172.         return builder.build();
  173.     }

  174.     /**
  175.      * Create the initial provider.
  176.      * <p>
  177.      * This method uses the internal {@code validBefore} flag to either return an invalid PVCoordinatesProvider or a
  178.      * constant one.
  179.      * </p>
  180.      *
  181.      * @param firstPoint the first waypoint
  182.      * @return the coordinate provider
  183.      */
  184.     protected PVCoordinatesProvider createInitial(final GeodeticPoint firstPoint) {
  185.         if (invalidBefore) {
  186.             return new AggregatedPVCoordinatesProvider.InvalidPVProvider();
  187.         } else {
  188.             return new ConstantPVCoordinatesProvider(firstPoint, body);
  189.         }
  190.     }

  191.     /**
  192.      * Create the final provider.
  193.      * <p>
  194.      * This method uses the internal {@code validAfter} flag to either return an invalid PVCoordinatesProvider or a
  195.      * constant one.
  196.      * </p>
  197.      *
  198.      * @param lastPoint the last waypoint
  199.      * @return the coordinate provider
  200.      */
  201.     protected PVCoordinatesProvider createFinal(final GeodeticPoint lastPoint) {
  202.         if (invalidAfter) {
  203.             return new AggregatedPVCoordinatesProvider.InvalidPVProvider();
  204.         } else {
  205.             return new ConstantPVCoordinatesProvider(lastPoint, body);
  206.         }
  207.     }

  208.     /**
  209.      * Factory interface, creating the {@link PVCoordinatesProvider} instances between the provided waypoints.
  210.      */
  211.     @FunctionalInterface
  212.     public interface InterpolationFactory {

  213.         /** Create a {@link PVCoordinatesProvider} which interpolates between the provided waypoints.
  214.          *
  215.          * @param date1 the first waypoint's date
  216.          * @param point1 the first waypoint's location
  217.          * @param date2 the second waypoint's date
  218.          * @param point2 the second waypoint's location
  219.          * @param body the body on which the waypoints are defined
  220.          * @return a {@link PVCoordinatesProvider} providing the locations at times between the waypoints.
  221.          */
  222.         PVCoordinatesProvider create(AbsoluteDate date1, GeodeticPoint point1,
  223.                                      AbsoluteDate date2, GeodeticPoint point2,
  224.                                      OneAxisEllipsoid body);
  225.     }

  226.     /**
  227.      * Coordinate provider interpolating along the great-circle between two points.
  228.      */
  229.     static class GreatCircleWaypointPVProv implements PVCoordinatesProvider {

  230.         /** Great circle estimation. */
  231.         private final Circle circle;
  232.         /** Duration between the two points (seconds). */
  233.         private final double duration;
  234.         /** Phase along the circle of the first point. */
  235.         private final double phase0;
  236.         /** Phase length from the first point to the second. */
  237.         private final double phaseLength;
  238.         /** Time at which interpolation results in the initial point. */
  239.         private final AbsoluteDate t0;
  240.         /** Body on which the great circle is defined. */
  241.         private final OneAxisEllipsoid body;
  242.         /** Phase of one second. */
  243.         private final double oneSecondPhase;
  244.         /** Altitude of the initial point. */
  245.         private final double initialAltitude;
  246.         /** Time-derivative of the altitude. */
  247.         private final double altitudeSlope;

  248.         /** Class constructor. Aligns to the {@link InterpolationFactory} functional interface.
  249.          *
  250.          * @param date1 the first waypoint's date
  251.          * @param point1 the first waypoint's location
  252.          * @param date2 the second waypoint's date
  253.          * @param point2 the second waypoint's location
  254.          * @param body the body on which the waypoints are defined
  255.          * @see InterpolationFactory
  256.          */
  257.         GreatCircleWaypointPVProv(final AbsoluteDate date1, final GeodeticPoint point1,
  258.                                   final AbsoluteDate date2, final GeodeticPoint point2,
  259.                                   final OneAxisEllipsoid body) {
  260.             this.t0 = date1;
  261.             this.duration = date2.durationFrom(date1);
  262.             this.body = body;
  263.             final S2Point s0 = toSpherical(point1);
  264.             final S2Point s1 = toSpherical(point2);
  265.             circle = new Circle(s0, s1, 1e-9);

  266.             phase0 = circle.getPhase(s0.getVector());
  267.             phaseLength = circle.getPhase(s1.getVector()) - phase0;

  268.             oneSecondPhase = phaseLength / duration;
  269.             altitudeSlope = (point2.getAltitude() - point1.getAltitude()) / duration;
  270.             initialAltitude = point1.getAltitude();
  271.         }

  272.         @Override
  273.         public Vector3D getPosition(final AbsoluteDate date, final Frame frame) {
  274.             final double d = date.durationFrom(t0);
  275.             final double fraction = d / duration;
  276.             final double phase = fraction * phaseLength;

  277.             final S2Point sp = new S2Point(circle.getPointAt(phase0 + phase));
  278.             final GeodeticPoint point = toGeodetic(sp, initialAltitude + d * altitudeSlope);
  279.             final Vector3D p = body.transform(point);

  280.             return body.getBodyFrame().getStaticTransformTo(frame, date).transformPosition(p);

  281.         }

  282.         @Override
  283.         public TimeStampedPVCoordinates getPVCoordinates(final AbsoluteDate date, final Frame frame) {
  284.             final double d = date.durationFrom(t0);
  285.             final double fraction = d / duration;
  286.             final double phase = fraction * phaseLength;

  287.             final S2Point sp = new S2Point(circle.getPointAt(phase0 + phase));
  288.             final GeodeticPoint point = toGeodetic(sp, initialAltitude + d * altitudeSlope);
  289.             final Vector3D p = body.transform(point);

  290.             // add 1 second to get another point along the circle, to use for velocity
  291.             final S2Point sp2 = new S2Point(circle.getPointAt(phase0 + phase + oneSecondPhase));
  292.             final GeodeticPoint point2 = toGeodetic(sp2, initialAltitude + (d + 1) * altitudeSlope);
  293.             final Vector3D p2 = body.transform(point2);
  294.             final Vector3D v = p2.subtract(p);

  295.             final TimeStampedPVCoordinates tpv = new TimeStampedPVCoordinates(date, p, v);
  296.             return body.getBodyFrame().getTransformTo(frame, date).transformPVCoordinates(tpv);
  297.         }

  298.         /** Converts the given geodetic point to a point on the 2-sphere.
  299.          * @param point input geodetic point
  300.          * @return a point on the 2-sphere
  301.          */
  302.         static S2Point toSpherical(final GeodeticPoint point) {
  303.             return new S2Point(point.getLongitude(), 0.5 * FastMath.PI - point.getLatitude());
  304.         }

  305.         /** Converts a 2-sphere point to a geodetic point.
  306.          * @param point point on the 2-sphere
  307.          * @param alt point altitude
  308.          * @return a geodetic point
  309.          */
  310.         static GeodeticPoint toGeodetic(final S2Point point, final double alt) {
  311.             return new GeodeticPoint(0.5 * FastMath.PI - point.getPhi(), point.getTheta(), alt);
  312.         }
  313.     }

  314.     /**
  315.      * Coordinate provider interpolating along the loxodrome between two points.
  316.      */
  317.     static class LoxodromeWaypointPVProv implements PVCoordinatesProvider {

  318.         /** Arc along which the interpolation occurs. */
  319.         private final LoxodromeArc arc;
  320.         /** Time at which the interpolation begins (at arc start). */
  321.         private final AbsoluteDate t0;
  322.         /** Total duration to get the length of the arc (seconds). */
  323.         private final double duration;
  324.         /** Velocity along the arc (m/s). */
  325.         private final double velocity;

  326.         /** Class constructor. Aligns to the {@link InterpolationFactory} functional interface.
  327.          *
  328.          * @param date1 the first waypoint's date
  329.          * @param point1 the first waypoint's location
  330.          * @param date2 the second waypoint's date
  331.          * @param point2 the second waypoint's location
  332.          * @param body the body on which the waypoints are defined
  333.          * @see InterpolationFactory
  334.          */
  335.         LoxodromeWaypointPVProv(final AbsoluteDate date1, final GeodeticPoint point1, final AbsoluteDate date2,
  336.                 final GeodeticPoint point2, final OneAxisEllipsoid body) {
  337.             this.arc = new LoxodromeArc(point1, point2, body);
  338.             this.t0 = date1;
  339.             this.duration = date2.durationFrom(date1);
  340.             this.velocity = arc.getDistance() / duration;
  341.         }

  342.         @Override
  343.         public Vector3D getPosition(final AbsoluteDate date, final Frame frame) {
  344.             final double fraction = date.durationFrom(t0) / duration;
  345.             final GeodeticPoint point = arc.calculatePointAlongArc(fraction);
  346.             final Vector3D p = arc.getBody().transform(point);

  347.             return arc.getBody().getBodyFrame().getStaticTransformTo(frame, date).transformPosition(p);
  348.         }

  349.         @Override
  350.         public TimeStampedPVCoordinates getPVCoordinates(final AbsoluteDate date, final Frame frame) {
  351.             final double fraction = date.durationFrom(t0) / duration;
  352.             final GeodeticPoint point = arc.calculatePointAlongArc(fraction);
  353.             final Vector3D p = arc.getBody().transform(point);
  354.             final Vector3D vp = arc.getBody().transform(
  355.                     new TopocentricFrame(arc.getBody(), point, "frame")
  356.                         .pointAtDistance(arc.getAzimuth(), 0, velocity));

  357.             final TimeStampedPVCoordinates tpv = new TimeStampedPVCoordinates(date, p, vp.subtract(p));
  358.             return arc.getBody().getBodyFrame().getTransformTo(frame, date).transformPVCoordinates(tpv);
  359.         }
  360.     }

  361.     /**
  362.      * Coordinate provider interpolating along the cartesian (3-space) line between two points.
  363.      */
  364.     static class CartesianWaypointPVProv implements PVCoordinatesProvider {

  365.         /** Date at which the position is valid. */
  366.         private final AbsoluteDate t0;
  367.         /** Initial point. */
  368.         private final Vector3D p0;
  369.         /** Velocity. */
  370.         private final Vector3D vel;
  371.         /** Frame in which the point and velocity are defined. */
  372.         private final Frame sourceFrame;

  373.         /** Class constructor. Aligns to the {@link InterpolationFactory} functional interface.
  374.          *
  375.          * @param date1 the first waypoint's date
  376.          * @param point1 the first waypoint's location
  377.          * @param date2 the second waypoint's date
  378.          * @param point2 the second waypoint's location
  379.          * @param body the body on which the waypoints are defined
  380.          * @see InterpolationFactory
  381.          */
  382.         CartesianWaypointPVProv(final AbsoluteDate date1, final GeodeticPoint point1,
  383.                                 final AbsoluteDate date2, final GeodeticPoint point2,
  384.                                 final OneAxisEllipsoid body) {
  385.             this.t0 = date1;
  386.             this.p0 = body.transform(point1);
  387.             this.vel = body.transform(point2).subtract(p0).scalarMultiply(1. / date2.durationFrom(t0));
  388.             this.sourceFrame = body.getBodyFrame();
  389.         }

  390.         @Override
  391.         public Vector3D getPosition(final AbsoluteDate date, final Frame frame) {
  392.             final double d = date.durationFrom(t0);
  393.             final Vector3D p = p0.add(vel.scalarMultiply(d));
  394.             return sourceFrame.getStaticTransformTo(frame, date).transformPosition(p);
  395.         }

  396.         @Override
  397.         public TimeStampedPVCoordinates getPVCoordinates(final AbsoluteDate date, final Frame frame) {
  398.             final double d = date.durationFrom(t0);
  399.             final Vector3D p = p0.add(vel.scalarMultiply(d));
  400.             final TimeStampedPVCoordinates pv = new TimeStampedPVCoordinates(date, p, vel);
  401.             return sourceFrame.getTransformTo(frame, date).transformPVCoordinates(pv);
  402.         }

  403.     }
  404. }