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  
19  import java.util.Map.Entry;
20  import java.util.TreeMap;
21  
22  import org.hipparchus.geometry.euclidean.threed.Vector3D;
23  import org.hipparchus.geometry.spherical.twod.Circle;
24  import org.hipparchus.geometry.spherical.twod.S2Point;
25  import org.hipparchus.util.FastMath;
26  import org.orekit.bodies.GeodeticPoint;
27  import org.orekit.bodies.LoxodromeArc;
28  import org.orekit.bodies.OneAxisEllipsoid;
29  import org.orekit.frames.Frame;
30  import org.orekit.frames.TopocentricFrame;
31  import org.orekit.time.AbsoluteDate;
32  import org.orekit.time.TimeOffset;
33  
34  /** Builder class, enabling incremental building of an {@link PVCoordinatesProvider}
35   * instance using waypoints defined on an ellipsoid.
36   * <p>
37   * Given a series of waypoints ({@code (date, point)} tuples),
38   * build a {@link PVCoordinatesProvider} representing the path.
39   * The static methods provide implementations for the most common path definitions
40   * (cartesian, great-circle, loxodrome). If these methods are insufficient,
41   * the public constructor provides a way to customize the path definition.
42   * </p>
43   * <p>
44   * This class connects the path segments using the {@link AggregatedPVCoordinatesProvider}.
45   * As such, no effort is made to smooth the velocity between segments.
46   * While position is unaffected, the velocity may be discontinuous between adjacent time points.
47   * Thus, care should be taken when modeling paths with abrupt direction changes
48   * (e.g. fast-moving aircraft); understand how the {@link PVCoordinatesProvider}
49   * will be used in the particular application.
50   * </p>
51   * @author Joe Reed
52   * @since 11.3
53   */
54  public class WaypointPVBuilder {
55  
56      /** Factory used to create intermediate pv providers between waypoints. */
57      private final InterpolationFactory factory;
58  
59      /** Central body, on which the waypoints are defined. */
60      private final OneAxisEllipsoid body;
61  
62      /** Set of waypoints, indexed by time. */
63      private final TreeMap<AbsoluteDate, GeodeticPoint> waypoints;
64  
65      /** Whether the resulting provider should be invalid or constant prior to the first waypoint. */
66      private boolean invalidBefore;
67  
68      /** Whether the resulting provider should be invalid or constant after to the last waypoint. */
69      private boolean invalidAfter;
70  
71      /** Create a new instance.
72       * @param factory The factory used to create the intermediate coordinate providers between waypoints.
73       * @param body The central body, on which the way points are defined.
74       */
75      public WaypointPVBuilder(final InterpolationFactory factory, final OneAxisEllipsoid body) {
76          this.factory       = factory;
77          this.body          = body;
78          this.waypoints     = new TreeMap<>();
79          this.invalidBefore = true;
80          this.invalidAfter  = true;
81      }
82  
83      /** Construct a waypoint builder interpolating points using a linear cartesian interpolation.
84       *
85       * @param body the reference ellipsoid on which the waypoints are defined.
86       * @return the waypoint builder
87       */
88      public static WaypointPVBuilder cartesianBuilder(final OneAxisEllipsoid body) {
89          return new WaypointPVBuilder(CartesianWaypointPVProv::new, body);
90      }
91  
92      /** Construct a waypoint builder interpolating points using a loxodrome (or Rhumbline).
93       *
94       * @param body the reference ellipsoid on which the waypoints are defined.
95       * @return the waypoint builder
96       */
97      public static WaypointPVBuilder loxodromeBuilder(final OneAxisEllipsoid body) {
98          return new WaypointPVBuilder(LoxodromeWaypointPVProv::new, body);
99      }
100 
101     /** Construct a waypoint builder interpolating points using a great-circle.
102      * <p>
103      * The altitude of the intermediate points is linearly interpolated from the bounding waypoints.
104      * Extrapolating before the first waypoint or after the last waypoint may result in undefined altitudes.
105      * </p>
106      * @param body the reference ellipsoid on which the waypoints are defined.
107      * @return the waypoint builder
108      */
109     public static WaypointPVBuilder greatCircleBuilder(final OneAxisEllipsoid body) {
110         return new WaypointPVBuilder(GreatCircleWaypointPVProv::new, body);
111     }
112 
113     /** Add a waypoint.
114      *
115      * @param point the waypoint location
116      * @param date the waypoint time
117      * @return this instance
118      */
119     public WaypointPVBuilder addWaypoint(final GeodeticPoint point, final AbsoluteDate date) {
120         waypoints.put(date, point);
121         return this;
122     }
123 
124     /** Indicate the resulting {@link PVCoordinatesProvider} should be invalid before the first waypoint.
125      *
126      * @return this instance
127      */
128     public WaypointPVBuilder invalidBefore() {
129         invalidBefore = true;
130         return this;
131     }
132 
133     /** Indicate the resulting {@link PVCoordinatesProvider} provide
134      * a constant location of the first waypoint prior to the first time.
135      *
136      * @return this instance
137      */
138     public WaypointPVBuilder constantBefore() {
139         invalidBefore = false;
140         return this;
141     }
142 
143     /** Indicate the resulting {@link PVCoordinatesProvider} should be invalid after the last waypoint.
144      *
145      * @return this instance
146      */
147     public WaypointPVBuilder invalidAfter() {
148         invalidAfter = true;
149         return this;
150     }
151 
152     /** Indicate the resulting {@link PVCoordinatesProvider} provide
153      * a constant location of the last waypoint after to the last time.
154      *
155      * @return this instance
156      */
157     public WaypointPVBuilder constantAfter() {
158         invalidAfter = false;
159         return this;
160     }
161 
162     /** Build a {@link PVCoordinatesProvider} from the waypoints added to this builder.
163      *
164      * @return the coordinates provider instance.
165      */
166     public PVCoordinatesProvider build() {
167         final PVCoordinatesProvider initialProvider = createInitial(waypoints.firstEntry().getValue());
168         final AggregatedPVCoordinatesProvider.Builder builder = new AggregatedPVCoordinatesProvider.Builder(initialProvider);
169 
170         Entry<AbsoluteDate, GeodeticPoint> previousEntry = null;
171         for (final Entry<AbsoluteDate, GeodeticPoint> entry: waypoints.entrySet()) {
172             if (previousEntry != null) {
173                 builder.addPVProviderAfter(previousEntry.getKey(),
174                                            factory.create(previousEntry.getKey(),
175                                                           previousEntry.getValue(),
176                                                           entry.getKey(),
177                                                           entry.getValue(),
178                                                           body),
179                                            true);
180             }
181             previousEntry = entry;
182         }
183         // add the point so we're valid at the final waypoint
184         builder.addPVProviderAfter(previousEntry.getKey(),
185                                    new ConstantPVCoordinatesProvider(previousEntry.getValue(), body),
186                                    true);
187         // add the final provider after the final waypoint
188         builder.addPVProviderAfter(previousEntry.getKey().shiftedBy(TimeOffset.ATTOSECOND),
189                                    createFinal(previousEntry.getValue()),
190                                    true);
191 
192         return builder.build();
193     }
194 
195     /**
196      * Create the initial provider.
197      * <p>
198      * This method uses the internal {@code validBefore} flag to either return an invalid PVCoordinatesProvider or a
199      * constant one.
200      * </p>
201      *
202      * @param firstPoint the first waypoint
203      * @return the coordinate provider
204      */
205     protected PVCoordinatesProvider createInitial(final GeodeticPoint firstPoint) {
206         if (invalidBefore) {
207             return new AggregatedPVCoordinatesProvider.InvalidPVProvider();
208         } else {
209             return new ConstantPVCoordinatesProvider(firstPoint, body);
210         }
211     }
212 
213     /**
214      * Create the final provider.
215      * <p>
216      * This method uses the internal {@code validAfter} flag to either return an invalid PVCoordinatesProvider or a
217      * constant one.
218      * </p>
219      *
220      * @param lastPoint the last waypoint
221      * @return the coordinate provider
222      */
223     protected PVCoordinatesProvider createFinal(final GeodeticPoint lastPoint) {
224         if (invalidAfter) {
225             return new AggregatedPVCoordinatesProvider.InvalidPVProvider();
226         } else {
227             return new ConstantPVCoordinatesProvider(lastPoint, body);
228         }
229     }
230 
231     /**
232      * Factory interface, creating the {@link PVCoordinatesProvider} instances between the provided waypoints.
233      */
234     @FunctionalInterface
235     public interface InterpolationFactory {
236 
237         /** Create a {@link PVCoordinatesProvider} which interpolates between the provided waypoints.
238          *
239          * @param date1 the first waypoint's date
240          * @param point1 the first waypoint's location
241          * @param date2 the second waypoint's date
242          * @param point2 the second waypoint's location
243          * @param body the body on which the waypoints are defined
244          * @return a {@link PVCoordinatesProvider} providing the locations at times between the waypoints.
245          */
246         PVCoordinatesProvider create(AbsoluteDate date1, GeodeticPoint point1,
247                                      AbsoluteDate date2, GeodeticPoint point2,
248                                      OneAxisEllipsoid body);
249     }
250 
251     /**
252      * Coordinate provider interpolating along the great-circle between two points.
253      */
254     static class GreatCircleWaypointPVProv implements PVCoordinatesProvider {
255 
256         /** Great circle estimation. */
257         private final Circle circle;
258         /** Duration between the two points (seconds). */
259         private final double duration;
260         /** Phase along the circle of the first point. */
261         private final double phase0;
262         /** Phase length from the first point to the second. */
263         private final double phaseLength;
264         /** Time at which interpolation results in the initial point. */
265         private final AbsoluteDate t0;
266         /** Body on which the great circle is defined. */
267         private final OneAxisEllipsoid body;
268         /** Phase of one second. */
269         private final double oneSecondPhase;
270         /** Altitude of the initial point. */
271         private final double initialAltitude;
272         /** Time-derivative of the altitude. */
273         private final double altitudeSlope;
274 
275         /** Class constructor. Aligns to the {@link InterpolationFactory} functional interface.
276          *
277          * @param date1 the first waypoint's date
278          * @param point1 the first waypoint's location
279          * @param date2 the second waypoint's date
280          * @param point2 the second waypoint's location
281          * @param body the body on which the waypoints are defined
282          * @see InterpolationFactory
283          */
284         GreatCircleWaypointPVProv(final AbsoluteDate date1, final GeodeticPoint point1,
285                                   final AbsoluteDate date2, final GeodeticPoint point2,
286                                   final OneAxisEllipsoid body) {
287             this.t0 = date1;
288             this.duration = date2.durationFrom(date1);
289             this.body = body;
290             final S2Point s0 = toSpherical(point1);
291             final S2Point s1 = toSpherical(point2);
292             circle = new Circle(s0, s1, 1e-9);
293 
294             phase0 = circle.getPhase(s0.getVector());
295             phaseLength = circle.getPhase(s1.getVector()) - phase0;
296 
297             oneSecondPhase = phaseLength / duration;
298             altitudeSlope = (point2.getAltitude() - point1.getAltitude()) / duration;
299             initialAltitude = point1.getAltitude();
300         }
301 
302         @Override
303         public Vector3D getPosition(final AbsoluteDate date, final Frame frame) {
304             final double d = date.durationFrom(t0);
305             final double fraction = d / duration;
306             final double phase = fraction * phaseLength;
307 
308             final S2Point sp = new S2Point(circle.getPointAt(phase0 + phase));
309             final GeodeticPoint point = toGeodetic(sp, initialAltitude + d * altitudeSlope);
310             final Vector3D p = body.transform(point);
311 
312             return body.getBodyFrame().getStaticTransformTo(frame, date).transformPosition(p);
313 
314         }
315 
316         @Override
317         public TimeStampedPVCoordinates getPVCoordinates(final AbsoluteDate date, final Frame frame) {
318             final double d = date.durationFrom(t0);
319             final double fraction = d / duration;
320             final double phase = fraction * phaseLength;
321 
322             final S2Point sp = new S2Point(circle.getPointAt(phase0 + phase));
323             final GeodeticPoint point = toGeodetic(sp, initialAltitude + d * altitudeSlope);
324             final Vector3D p = body.transform(point);
325 
326             // add 1 second to get another point along the circle, to use for velocity
327             final S2Point sp2 = new S2Point(circle.getPointAt(phase0 + phase + oneSecondPhase));
328             final GeodeticPoint point2 = toGeodetic(sp2, initialAltitude + (d + 1) * altitudeSlope);
329             final Vector3D p2 = body.transform(point2);
330             final Vector3D v = p2.subtract(p);
331 
332             final TimeStampedPVCoordinates tpv = new TimeStampedPVCoordinates(date, p, v);
333             return body.getBodyFrame().getTransformTo(frame, date).transformPVCoordinates(tpv);
334         }
335 
336         /** Converts the given geodetic point to a point on the 2-sphere.
337          * @param point input geodetic point
338          * @return a point on the 2-sphere
339          */
340         static S2Point toSpherical(final GeodeticPoint point) {
341             return new S2Point(point.getLongitude(), 0.5 * FastMath.PI - point.getLatitude());
342         }
343 
344         /** Converts a 2-sphere point to a geodetic point.
345          * @param point point on the 2-sphere
346          * @param alt point altitude
347          * @return a geodetic point
348          */
349         static GeodeticPoint toGeodetic(final S2Point point, final double alt) {
350             return new GeodeticPoint(0.5 * FastMath.PI - point.getPhi(), point.getTheta(), alt);
351         }
352     }
353 
354     /**
355      * Coordinate provider interpolating along the loxodrome between two points.
356      */
357     static class LoxodromeWaypointPVProv implements PVCoordinatesProvider {
358 
359         /** Arc along which the interpolation occurs. */
360         private final LoxodromeArc arc;
361         /** Time at which the interpolation begins (at arc start). */
362         private final AbsoluteDate t0;
363         /** Total duration to get the length of the arc (seconds). */
364         private final double duration;
365         /** Velocity along the arc (m/s). */
366         private final double velocity;
367 
368         /** Class constructor. Aligns to the {@link InterpolationFactory} functional interface.
369          *
370          * @param date1 the first waypoint's date
371          * @param point1 the first waypoint's location
372          * @param date2 the second waypoint's date
373          * @param point2 the second waypoint's location
374          * @param body the body on which the waypoints are defined
375          * @see InterpolationFactory
376          */
377         LoxodromeWaypointPVProv(final AbsoluteDate date1, final GeodeticPoint point1, final AbsoluteDate date2,
378                 final GeodeticPoint point2, final OneAxisEllipsoid body) {
379             this.arc = new LoxodromeArc(point1, point2, body);
380             this.t0 = date1;
381             this.duration = date2.durationFrom(date1);
382             this.velocity = arc.getDistance() / duration;
383         }
384 
385         @Override
386         public Vector3D getPosition(final AbsoluteDate date, final Frame frame) {
387             final double fraction = date.durationFrom(t0) / duration;
388             final GeodeticPoint point = arc.calculatePointAlongArc(fraction);
389             final Vector3D p = arc.getBody().transform(point);
390 
391             return arc.getBody().getBodyFrame().getStaticTransformTo(frame, date).transformPosition(p);
392         }
393 
394         @Override
395         public TimeStampedPVCoordinates getPVCoordinates(final AbsoluteDate date, final Frame frame) {
396             final double fraction = date.durationFrom(t0) / duration;
397             final GeodeticPoint point = arc.calculatePointAlongArc(fraction);
398             final Vector3D p = arc.getBody().transform(point);
399             final Vector3D vp = arc.getBody().transform(
400                     new TopocentricFrame(arc.getBody(), point, "frame")
401                         .pointAtDistance(arc.getAzimuth(), 0, velocity));
402 
403             final TimeStampedPVCoordinates tpv = new TimeStampedPVCoordinates(date, p, vp.subtract(p));
404             return arc.getBody().getBodyFrame().getTransformTo(frame, date).transformPVCoordinates(tpv);
405         }
406     }
407 
408     /**
409      * Coordinate provider interpolating along the cartesian (3-space) line between two points.
410      */
411     static class CartesianWaypointPVProv implements PVCoordinatesProvider {
412 
413         /** Date at which the position is valid. */
414         private final AbsoluteDate t0;
415         /** Initial point. */
416         private final Vector3D p0;
417         /** Velocity. */
418         private final Vector3D vel;
419         /** Frame in which the point and velocity are defined. */
420         private final Frame sourceFrame;
421 
422         /** Class constructor. Aligns to the {@link InterpolationFactory} functional interface.
423          *
424          * @param date1 the first waypoint's date
425          * @param point1 the first waypoint's location
426          * @param date2 the second waypoint's date
427          * @param point2 the second waypoint's location
428          * @param body the body on which the waypoints are defined
429          * @see InterpolationFactory
430          */
431         CartesianWaypointPVProv(final AbsoluteDate date1, final GeodeticPoint point1,
432                                 final AbsoluteDate date2, final GeodeticPoint point2,
433                                 final OneAxisEllipsoid body) {
434             this.t0 = date1;
435             this.p0 = body.transform(point1);
436             this.vel = body.transform(point2).subtract(p0).scalarMultiply(1. / date2.durationFrom(t0));
437             this.sourceFrame = body.getBodyFrame();
438         }
439 
440         @Override
441         public Vector3D getPosition(final AbsoluteDate date, final Frame frame) {
442             final double d = date.durationFrom(t0);
443             final Vector3D p = p0.add(vel.scalarMultiply(d));
444             return sourceFrame.getStaticTransformTo(frame, date).transformPosition(p);
445         }
446 
447         @Override
448         public TimeStampedPVCoordinates getPVCoordinates(final AbsoluteDate date, final Frame frame) {
449             final double d = date.durationFrom(t0);
450             final Vector3D p = p0.add(vel.scalarMultiply(d));
451             final TimeStampedPVCoordinates pv = new TimeStampedPVCoordinates(date, p, vel);
452             return sourceFrame.getTransformTo(frame, date).transformPVCoordinates(pv);
453         }
454 
455     }
456 }