1   /* Copyright 2002-2026 CS GROUP
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    * CS 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.frames;
18  
19  import org.hipparchus.Field;
20  import org.hipparchus.CalculusFieldElement;
21  import org.hipparchus.analysis.UnivariateFunction;
22  import org.hipparchus.analysis.solvers.BracketingNthOrderBrentSolver;
23  import org.hipparchus.analysis.solvers.UnivariateSolver;
24  import org.hipparchus.exception.MathRuntimeException;
25  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
26  import org.hipparchus.geometry.euclidean.threed.Rotation;
27  import org.hipparchus.geometry.euclidean.threed.Vector3D;
28  import org.hipparchus.util.FastMath;
29  import org.hipparchus.util.FieldSinCos;
30  import org.hipparchus.util.MathUtils;
31  import org.hipparchus.util.SinCos;
32  import org.orekit.bodies.BodyShape;
33  import org.orekit.bodies.FieldGeodeticPoint;
34  import org.orekit.bodies.GeodeticPoint;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.time.AbsoluteDate;
37  import org.orekit.time.FieldAbsoluteDate;
38  import org.orekit.utils.Constants;
39  import org.orekit.utils.ExtendedPositionProvider;
40  import org.orekit.utils.FieldPVCoordinates;
41  import org.orekit.utils.FieldTrackingCoordinates;
42  import org.orekit.utils.PVCoordinates;
43  import org.orekit.utils.TimeStampedFieldPVCoordinates;
44  import org.orekit.utils.TimeStampedPVCoordinates;
45  import org.orekit.utils.TrackingCoordinates;
46  
47  
48  /** Topocentric frame.
49   * <p>Frame associated to a position near the surface of a body shape.</p>
50   * <p>
51   * The origin of the frame is at the defining {@link GeodeticPoint geodetic point}
52   * location, and the right-handed canonical trihedra is:
53   * </p>
54   * <ul>
55   *   <li>X axis in the local horizontal plane (normal to zenith direction) and
56   *   following the local parallel towards East</li>
57   *   <li>Y axis in the horizontal plane (normal to zenith direction) and
58   *   following the local meridian towards North</li>
59   *   <li>Z axis towards Zenith direction</li>
60   * </ul>
61   * @author V&eacute;ronique Pommier-Maurussane
62   * @see org.orekit.utils.GeodeticExtendedPositionProvider
63   */
64  public class TopocentricFrame extends Frame implements ExtendedPositionProvider {
65  
66      /** Body shape on which the local point is defined. */
67      private final BodyShape parentShape;
68  
69      /** Geodetic point where the topocentric frame is defined. */
70      private final GeodeticPoint point;
71  
72      /** Cartesian point where the topocentric frame is defined.
73       * @since 12.0
74       */
75      private final Vector3D cartesianPoint;
76  
77      /** Simple constructor.
78       * @param parentShape body shape on which the local point is defined
79       * @param point local surface point where topocentric frame is defined
80       * @param name the string representation
81       */
82      public TopocentricFrame(final BodyShape parentShape, final GeodeticPoint point,
83                              final String name) {
84  
85          super(parentShape.getBodyFrame(),
86                  new Transform(AbsoluteDate.ARBITRARY_EPOCH,
87                          new Transform(AbsoluteDate.ARBITRARY_EPOCH,
88                                  parentShape.transform(point).negate()),
89                          new Transform(AbsoluteDate.ARBITRARY_EPOCH,
90                                  new Rotation(point.getEast(), point.getZenith(),
91                                          Vector3D.PLUS_I, Vector3D.PLUS_K),
92                                  Vector3D.ZERO)),
93                  name, false);
94          this.parentShape    = parentShape;
95          this.point          = point;
96          this.cartesianPoint = getTransformProvider().
97                  getStaticTransform(AbsoluteDate.ARBITRARY_EPOCH).
98                  getInverse().
99                  transformPosition(Vector3D.ZERO);
100     }
101 
102     /** Get the body shape on which the local point is defined.
103      * @return body shape on which the local point is defined
104      */
105     public BodyShape getParentShape() {
106         return parentShape;
107     }
108 
109     /** Get the surface point defining the origin of the frame.
110      * @return surface point defining the origin of the frame
111      */
112     public GeodeticPoint getPoint() {
113         return point;
114     }
115 
116     /** Get the surface point defining the origin of the frame.
117      * @return surface point defining the origin of the frame in body frame
118      * @since 12.0
119      */
120     public Vector3D getCartesianPoint() {
121         return cartesianPoint;
122     }
123 
124     /** Get the surface point defining the origin of the frame.
125      * @param <T> type of the elements
126      * @param field of the elements
127      * @return surface point defining the origin of the frame
128      * @since 9.3
129      */
130     public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> getPoint(final Field<T> field) {
131         return new FieldGeodeticPoint<>(field, point);
132     }
133 
134     /** Get the zenith direction of topocentric frame, expressed in parent shape frame.
135      * <p>The zenith direction is defined as the normal to local horizontal plane.</p>
136      * @return unit vector in the zenith direction
137      * @see #getNadir()
138      */
139     public Vector3D getZenith() {
140         return point.getZenith();
141     }
142 
143     /** Get the nadir direction of topocentric frame, expressed in parent shape frame.
144      * <p>The nadir direction is the opposite of zenith direction.</p>
145      * @return unit vector in the nadir direction
146      * @see #getZenith()
147      */
148     public Vector3D getNadir() {
149         return point.getNadir();
150     }
151 
152     /** Get the north direction of topocentric frame, expressed in parent shape frame.
153      * <p>The north direction is defined in the horizontal plane
154      * (normal to zenith direction) and following the local meridian.</p>
155      * @return unit vector in the north direction
156      * @see #getSouth()
157      */
158     public Vector3D getNorth() {
159         return point.getNorth();
160     }
161 
162     /** Get the south direction of topocentric frame, expressed in parent shape frame.
163      * <p>The south direction is the opposite of north direction.</p>
164      * @return unit vector in the south direction
165      * @see #getNorth()
166      */
167     public Vector3D getSouth() {
168         return point.getSouth();
169     }
170 
171     /** Get the east direction of topocentric frame, expressed in parent shape frame.
172      * <p>The east direction is defined in the horizontal plane
173      * in order to complete direct triangle (east, north, zenith).</p>
174      * @return unit vector in the east direction
175      * @see #getWest()
176      */
177     public Vector3D getEast() {
178         return point.getEast();
179     }
180 
181     /** Get the west direction of topocentric frame, expressed in parent shape frame.
182      * <p>The west direction is the opposite of east direction.</p>
183      * @return unit vector in the west direction
184      * @see #getEast()
185      */
186     public Vector3D getWest() {
187         return point.getWest();
188     }
189 
190     /** Get the tracking coordinates of a point with regards to the local point.
191      * @param extPoint point for which elevation shall be computed
192      * @param frame frame in which the point is defined
193      * @param date computation date
194      * @return tracking coordinates of the point
195      * @since 12.0
196      */
197     public TrackingCoordinates getTrackingCoordinates(final Vector3D extPoint, final Frame frame,
198                                                       final AbsoluteDate date) {
199 
200         // transform given point from given frame to topocentric frame
201         final Vector3D extPointTopo = transformPoint(extPoint, frame, date);
202 
203         final double azimuth = computeAzimuthFromTopoPoint(extPointTopo);
204 
205         return new TrackingCoordinates(azimuth, extPointTopo.getDelta(), extPointTopo.getNorm());
206 
207     }
208 
209     /** Get the tracking coordinates of a point with regards to the local point.
210      * @param <T> type of the field elements
211      * @param extPoint point for which elevation shall be computed
212      * @param frame frame in which the point is defined
213      * @param date computation date
214      * @return tracking coordinates of the point
215      * @since 12.0
216      */
217     public <T extends CalculusFieldElement<T>> FieldTrackingCoordinates<T> getTrackingCoordinates(final FieldVector3D<T> extPoint,
218                                                                                                   final Frame frame,
219                                                                                                   final FieldAbsoluteDate<T> date) {
220 
221         // Transform given point from given frame to topocentric frame
222         final FieldVector3D<T> extPointTopo = transformPoint(extPoint, frame, date);
223 
224         final T azimuth = computeAzimuthFromTopoPoint(extPointTopo);
225 
226         return new FieldTrackingCoordinates<>(azimuth, extPointTopo.getDelta(), extPointTopo.getNorm());
227 
228     }
229 
230     /** Get the elevation of a point with regards to the local point.
231      * <p>The elevation is the angle between the local horizontal and
232      * the direction from local point to given point.</p>
233      * @param extPoint point for which elevation shall be computed
234      * @param frame frame in which the point is defined
235      * @param date computation date
236      * @return elevation of the point
237      */
238     public double getElevation(final Vector3D extPoint, final Frame frame,
239                                final AbsoluteDate date) {
240 
241         // Transform given point from given frame to topocentric frame
242         final Vector3D extPointTopo = transformPoint(extPoint, frame, date);
243 
244         return extPointTopo.getDelta();
245     }
246 
247     /** Get the elevation of a point with regards to the local point.
248      * <p>The elevation is the angle between the local horizontal and
249      * the direction from local point to given point.</p>
250      * @param <T> type of the elements
251      * @param extPoint point for which elevation shall be computed
252      * @param frame frame in which the point is defined
253      * @param date computation date
254      * @return elevation of the point
255      * @since 9.3
256      */
257     public <T extends CalculusFieldElement<T>> T getElevation(final FieldVector3D<T> extPoint, final Frame frame,
258                                                               final FieldAbsoluteDate<T> date) {
259 
260         // Transform given point from given frame to topocentric frame
261         final FieldVector3D<T> extPointTopo = transformPoint(extPoint, frame, date);
262 
263         return extPointTopo.getDelta();
264     }
265 
266     /** Get the azimuth of a point with regards to the topocentric frame center point.
267      * <p>The azimuth is the angle between the North direction at local point and
268      * the projection in local horizontal plane of the direction from local point
269      * to given point. Azimuth angles are counted clockwise, i.e positive towards the East.</p>
270      * @param extPoint point for which elevation shall be computed
271      * @param frame frame in which the point is defined
272      * @param date computation date
273      * @return azimuth of the point
274      */
275     public double getAzimuth(final Vector3D extPoint, final Frame frame,
276                              final AbsoluteDate date) {
277 
278         // Transform given point from given frame to topocentric frame
279         final Vector3D extPointTopo = transformPoint(extPoint, frame, date);
280 
281         return computeAzimuthFromTopoPoint(extPointTopo);
282 
283     }
284 
285     /** Get the azimuth of a point with regards to the topocentric frame center point.
286      * <p>The azimuth is the angle between the North direction at local point and
287      * the projection in local horizontal plane of the direction from local point
288      * to given point. Azimuth angles are counted clockwise, i.e positive towards the East.</p>
289      * @param <T> type of the elements
290      * @param extPoint point for which elevation shall be computed
291      * @param frame frame in which the point is defined
292      * @param date computation date
293      * @return azimuth of the point
294      * @since 9.3
295      */
296     public <T extends CalculusFieldElement<T>> T getAzimuth(final FieldVector3D<T> extPoint, final Frame frame,
297                                                             final FieldAbsoluteDate<T> date) {
298 
299         // Transform given point from given frame to topocentric frame
300         final FieldVector3D<T> extPointTopo = transformPoint(extPoint, frame, date);
301 
302         return computeAzimuthFromTopoPoint(extPointTopo);
303 
304     }
305 
306     /** Get the range of a point with regards to the topocentric frame center point.
307      * @param extPoint point for which range shall be computed
308      * @param frame frame in which the point is defined
309      * @param date computation date
310      * @return range (distance) of the point
311      */
312     public double getRange(final Vector3D extPoint, final Frame frame,
313                            final AbsoluteDate date) {
314 
315         // Transform given point from given frame to topocentric frame
316         final Vector3D extPointTopo = transformPoint(extPoint, frame, date);
317 
318         return extPointTopo.getNorm();
319 
320     }
321 
322     /** Get the range of a point with regards to the topocentric frame center point.
323      * @param <T> type of the elements
324      * @param extPoint point for which range shall be computed
325      * @param frame frame in which the point is defined
326      * @param date computation date
327      * @return range (distance) of the point
328      * @since 9.3
329      */
330     public <T extends CalculusFieldElement<T>> T getRange(final FieldVector3D<T> extPoint, final Frame frame,
331                                                           final FieldAbsoluteDate<T> date) {
332 
333         // Transform given point from given frame to topocentric frame
334         final FieldVector3D<T> extPointTopo = transformPoint(extPoint, frame, date);
335 
336         return extPointTopo.getNorm();
337 
338     }
339 
340     /** Get the range rate of a point with regards to the topocentric frame center point.
341      * @param extPV point/velocity for which range rate shall be computed
342      * @param frame frame in which the point is defined
343      * @param date computation date
344      * @return range rate of the point (positive if point departs from frame)
345      */
346     public double getRangeRate(final PVCoordinates extPV, final Frame frame,
347                                final AbsoluteDate date) {
348 
349         // Transform given point from given frame to topocentric frame
350         final KinematicTransform t = frame.getKinematicTransformTo(this, date);
351         final PVCoordinates extPVTopo = t.transformOnlyPV(extPV);
352 
353         // Compute range rate (doppler) : relative rate along the line of sight
354         return Vector3D.dotProduct(extPVTopo.getPosition(), extPVTopo.getVelocity()) /
355                 extPVTopo.getPosition().getNorm();
356 
357     }
358 
359     /** Get the range rate of a point with regards to the topocentric frame center point.
360      * @param <T> type of the elements
361      * @param extPV point/velocity for which range rate shall be computed
362      * @param frame frame in which the point is defined
363      * @param date computation date
364      * @return range rate of the point (positive if point departs from frame)
365      * @since 9.3
366      */
367     public <T extends CalculusFieldElement<T>> T getRangeRate(final FieldPVCoordinates<T> extPV, final Frame frame,
368                                                               final FieldAbsoluteDate<T> date) {
369 
370         // Transform given point from given frame to topocentric frame
371         final FieldKinematicTransform<T> t = frame.getKinematicTransformTo(this, date);
372         final FieldPVCoordinates<T> extPVTopo = t.transformOnlyPV(extPV);
373 
374         // Compute range rate (doppler) : relative rate along the line of sight
375         return FieldVector3D.dotProduct(extPVTopo.getPosition(), extPVTopo.getVelocity()).divide(
376                 extPVTopo.getPosition().getNorm());
377 
378     }
379 
380     /**
381      * Compute the limit visibility point for a satellite in a given direction.
382      * <p>
383      * This method can be used to compute visibility circles around ground stations
384      * for example, using a simple loop on azimuth, with either a fixed elevation
385      * or an elevation that depends on azimuth to take ground masks into account.
386      * </p>
387      * @param radius satellite distance to Earth center
388      * @param azimuth pointing azimuth from station
389      * @param elevation pointing elevation from station
390      * @return limit visibility point for the satellite
391      */
392     public GeodeticPoint computeLimitVisibilityPoint(final double radius,
393                                                      final double azimuth, final double elevation) {
394         try {
395             // convergence threshold on point position: 1mm
396             final double deltaP = 0.001;
397             final UnivariateSolver solver =
398                     new BracketingNthOrderBrentSolver(deltaP / Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
399                             deltaP, deltaP, 5);
400 
401             // find the distance such that a point in the specified direction and at the solved-for
402             // distance is exactly at the specified radius
403             final double distance = solver.solve(1000, new UnivariateFunction() {
404                 /** {@inheritDoc} */
405                 public double value(final double x) {
406                     final GeodeticPoint gp = pointAtDistance(azimuth, elevation, x);
407                     return parentShape.transform(gp).getNorm() - radius;
408                 }
409             }, 0, 2 * radius);
410 
411             // return the limit point
412             return pointAtDistance(azimuth, elevation, distance);
413 
414         } catch (MathRuntimeException mrte) {
415             throw new OrekitException(mrte);
416         }
417     }
418 
419     /** Compute the point observed from the station at some specified distance.
420      * @param azimuth pointing azimuth from station
421      * @param elevation pointing elevation from station
422      * @param distance distance to station
423      * @return observed point
424      */
425     public GeodeticPoint pointAtDistance(final double azimuth, final double elevation,
426                                          final double distance) {
427         final SinCos scAz  = FastMath.sinCos(azimuth);
428         final SinCos scEl  = FastMath.sinCos(elevation);
429         final Vector3D  observed = new Vector3D(distance * scEl.cos() * scAz.sin(),
430                 distance * scEl.cos() * scAz.cos(),
431                 distance * scEl.sin());
432         return parentShape.transform(observed, this, AbsoluteDate.ARBITRARY_EPOCH);
433     }
434 
435     /** {@inheritDoc} */
436     @Override
437     public Vector3D getPosition(final AbsoluteDate date, final Frame frame) {
438         return getStaticTransformTo(frame, date).transformPosition(Vector3D.ZERO);
439     }
440 
441     /** {@inheritDoc} */
442     @Override
443     public <T extends CalculusFieldElement<T>> FieldVector3D<T> getPosition(final FieldAbsoluteDate<T> date,
444                                                                             final Frame frame) {
445         return getStaticTransformTo(frame, date).transformPosition(Vector3D.ZERO);
446     }
447 
448     /** {@inheritDoc} */
449     @Override
450     public Vector3D getVelocity(final AbsoluteDate date, final Frame frame) {
451         return getKinematicTransformTo(frame, date).transformOnlyPV(PVCoordinates.ZERO).getVelocity();
452     }
453 
454     /** {@inheritDoc} */
455     @Override
456     public <T extends CalculusFieldElement<T>> FieldVector3D<T> getVelocity(final FieldAbsoluteDate<T> date, final Frame frame) {
457         return getKinematicTransformTo(frame, date).transformOnlyPV(FieldPVCoordinates.getZero(date.getField())).getVelocity();
458     }
459 
460     /** {@inheritDoc} */
461     @Override
462     public TimeStampedPVCoordinates getPVCoordinates(final AbsoluteDate date, final Frame frame) {
463         return getTransformTo(frame, date).transformPVCoordinates(new TimeStampedPVCoordinates(date, PVCoordinates.ZERO));
464     }
465 
466     /** {@inheritDoc} */
467     @Override
468     public <T extends CalculusFieldElement<T>> TimeStampedFieldPVCoordinates<T> getPVCoordinates(final FieldAbsoluteDate<T> date,
469                                                                                                  final Frame frame) {
470         return getTransformTo(frame, date).transformPVCoordinates(new TimeStampedFieldPVCoordinates<>(date, FieldPVCoordinates.getZero(date.getField())));
471     }
472 
473     /** Get the topocentric position from {@link TrackingCoordinates}.
474      * @param coords The coordinates that are to be converted.
475      * @return The topocentric coordinates.
476      * @since 12.1
477      */
478     public static Vector3D getTopocentricPosition(final TrackingCoordinates coords) {
479         return getTopocentricPosition(coords.getAzimuth(), coords.getElevation(), coords.getRange());
480     }
481 
482     /** Get the topocentric position from {@link FieldTrackingCoordinates}.
483      * @param coords The coordinates that are to be converted.
484      * @param <T> Type of the field coordinates.
485      * @return The topocentric coordinates.
486      * @since 12.1
487      */
488     public static <T extends CalculusFieldElement<T>> FieldVector3D<T> getTopocentricPosition(final FieldTrackingCoordinates<T> coords) {
489         return getTopocentricPosition(coords.getAzimuth(), coords.getElevation(), coords.getRange());
490     }
491 
492     /**
493      * Gets the topocentric position from a set of az/el/ra coordinates.
494      * @param azimuth the angle of rotation around the vertical axis, going East.
495      * @param elevation the elevation angle from the local horizon.
496      * @param range the distance from the goedetic position.
497      * @return the topocentric position.
498      * @since 12.1
499      */
500     private static Vector3D getTopocentricPosition(final double azimuth, final double elevation, final double range) {
501         final SinCos sinCosAz = FastMath.sinCos(azimuth);
502         final SinCos sinCosEL = FastMath.sinCos(elevation);
503         return new Vector3D(range * sinCosEL.cos() * sinCosAz.sin(), range * sinCosEL.cos() * sinCosAz.cos(), range * sinCosEL.sin());
504     }
505 
506     /**
507      * Gets the topocentric position from a set of az/el/ra coordinates.
508      * @param azimuth the angle of rotation around the vertical axis, going East.
509      * @param elevation the elevation angle from the local horizon.
510      * @param range the distance from the geodetic position.
511      * @return the topocentric position.
512      * @param <T> the type of the az/el/ra coordinates.
513      * @since 12.1
514      */
515     private static <T extends CalculusFieldElement<T>> FieldVector3D<T> getTopocentricPosition(final T azimuth, final T elevation, final T range) {
516         final FieldSinCos<T> sinCosAz = FastMath.sinCos(azimuth);
517         final FieldSinCos<T> sinCosEl = FastMath.sinCos(elevation);
518         return new FieldVector3D<>(
519                 range.multiply(sinCosEl.cos()).multiply(sinCosAz.sin()),
520                 range.multiply(sinCosEl.cos()).multiply(sinCosAz.cos()),
521                 range.multiply(sinCosEl.sin())
522         );
523     }
524 
525     /** Transform point in topocentric frame.
526      * @param extPoint point
527      * @param date current date
528      * @param frame the frame where to define the position
529      * @return transformed point in topocentric frame
530      */
531     private Vector3D transformPoint(final Vector3D extPoint, final Frame frame, final AbsoluteDate date) {
532         final StaticTransform t = frame.getStaticTransformTo(this, date);
533         return t.transformPosition(extPoint);
534     }
535 
536     /** Transform point in topocentric frame.
537      * @param <T> type of the field elements
538      * @param extPoint point
539      * @param date current date
540      * @param frame the frame where to define the position
541      * @return transformed point in topocentric frame
542      */
543     private <T extends CalculusFieldElement<T>> FieldVector3D<T> transformPoint(final FieldVector3D<T> extPoint,
544                                                                                 final Frame frame,
545                                                                                 final FieldAbsoluteDate<T> date) {
546         final FieldStaticTransform<T> t = frame.getStaticTransformTo(this, date);
547         return t.transformPosition(extPoint);
548     }
549 
550     /** Compute azimuth from topocentric point.
551      * @param extPointTopo topocentric point
552      * @return azimuth
553      */
554     private double computeAzimuthFromTopoPoint(final Vector3D extPointTopo) {
555         final double azimuth = FastMath.atan2(extPointTopo.getX(), extPointTopo.getY());
556         if (azimuth < 0.0) {
557             return azimuth + MathUtils.TWO_PI;
558         } else {
559             return azimuth;
560         }
561     }
562 
563     /** Compute azimuth from topocentric point.
564      * @param <T> type of the field elements
565      * @param extPointTopo topocentric point
566      * @return azimuth
567      */
568     private <T extends CalculusFieldElement<T>> T computeAzimuthFromTopoPoint(final FieldVector3D<T> extPointTopo) {
569         final T azimuth = FastMath.atan2(extPointTopo.getX(), extPointTopo.getY());
570         if (azimuth.getReal() < 0.0) {
571             return azimuth.add(MathUtils.TWO_PI);
572         } else {
573             return azimuth;
574         }
575     }
576 
577 }