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 }