1   /* Copyright 2022-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.bodies;
18  
19  import org.hipparchus.util.FastMath;
20  import org.hipparchus.util.MathUtils;
21  
22  /** Loxodrome defined by a start and ending point.
23   *
24   * @author Joe Reed
25   * @since 11.3
26   */
27  public class LoxodromeArc extends Loxodrome {
28  
29      /** Threshold for considering latitudes equal, in radians. */
30      private static final double LATITUDE_THRESHOLD = 1e-6;
31  
32      /** Maximum number of iterations used when computing distance between points. */
33      private static final int MAX_ITER = 50;
34  
35      /** Ending point of the arc. */
36      private final GeodeticPoint endPoint;
37  
38      /** Delta longitude, cached, radians. */
39      private final double deltaLon;
40  
41      /** Cached arc distance, meters. */
42      private double distance = -1;
43  
44      /** Class constructor where the arc's altitude is the average of the initial and final points.
45       * @param point the starting point
46       * @param endPoint the ending point
47       * @param body the body on which the loxodrome is defined
48       */
49      public LoxodromeArc(final GeodeticPoint point, final GeodeticPoint endPoint, final OneAxisEllipsoid body) {
50          this(point, endPoint, body, (point.getAltitude() + endPoint.getAltitude()) / 2.);
51      }
52  
53      /** Class constructor.
54       * @param point the starting point
55       * @param endPoint the ending point
56       * @param body the body on which the loxodrome is defined
57       * @param altitude the altitude above the reference body (meters)
58       */
59      public LoxodromeArc(final GeodeticPoint point, final GeodeticPoint endPoint, final OneAxisEllipsoid body,
60                          final double altitude) {
61          super(point, body.azimuthBetweenPoints(point, endPoint), body, altitude);
62          this.endPoint = endPoint;
63          this.deltaLon = MathUtils.normalizeAngle(endPoint.getLongitude(), point.getLongitude()) -
64                          point.getLongitude();
65      }
66  
67      /** Get the final point of the arc.
68       *
69       * @return the ending point of the arc
70       */
71      public GeodeticPoint getFinalPoint() {
72          return this.endPoint;
73      }
74  
75      /** Compute the distance of the arc along the surface of the ellipsoid.
76       *
77       * @return the distance (meters)
78       */
79      public double getDistance() {
80          if (distance >= 0) {
81              return distance;
82          }
83  
84          // compute the e sin(lat)^2
85          final double ptLat  = getPoint().getLatitude();
86          final double sinLat = FastMath.sin(ptLat);
87          final double eccSinLatSq = getBody().getEccentricitySquared() * sinLat * sinLat;
88  
89          // compute intermediate values
90          final double t1 = 1. - getBody().getEccentricitySquared();
91          final double t2 = 1. - eccSinLatSq;
92          final double t3 = FastMath.sqrt(t2);
93  
94          final double semiMajorAxis = getBody().getEquatorialRadius() + getAltitude();
95  
96          final double meridianCurve = (semiMajorAxis * t1) / (t2 * t3);
97  
98          if (FastMath.abs(endPoint.getLatitude() - ptLat) < LATITUDE_THRESHOLD) {
99              distance = (semiMajorAxis / t3) * FastMath.abs(FastMath.cos(ptLat) * deltaLon);
100         }
101         else {
102             final double eccSq34 = 0.75 * getBody().getEccentricitySquared();
103             final double halfEccSq34 = eccSq34 / 2.;
104             final double t6 = 1. / (1. - eccSq34);
105             final double t7 = t1 * semiMajorAxis / meridianCurve;
106 
107             final double t8 = ptLat + t6 *
108                 (t7 * (endPoint.getLatitude() - ptLat) + halfEccSq34 * FastMath.sin(ptLat * 2.));
109             final double t9 = halfEccSq34 * t6;
110 
111             double guess = 0;
112             double lat = endPoint.getLatitude();
113             for (int i = 0; i < MAX_ITER; i++) {
114                 guess = lat;
115                 lat = t8 - t9 * FastMath.sin(2. * guess);
116 
117                 if (FastMath.abs(lat - guess) < LATITUDE_THRESHOLD) {
118                     break;
119                 }
120             }
121 
122             final double azimuth = FastMath.atan2(deltaLon,
123                 getBody().geodeticToIsometricLatitude(lat) - getBody().geodeticToIsometricLatitude(ptLat));
124             distance = meridianCurve * FastMath.abs((lat - ptLat) / FastMath.cos(azimuth));
125         }
126 
127         return distance;
128     }
129 
130     /** Calculate a point at a specific percentage along the arc.
131      *
132      * @param fraction the fraction along the arc to compute the point
133      * @return the point along the arc
134      */
135     public GeodeticPoint calculatePointAlongArc(final double fraction) {
136         if (fraction == 0.) {
137             return getPoint();
138         }
139         else if (fraction == 1.) {
140             return getFinalPoint();
141         }
142         else {
143             final double d = getDistance() * fraction;
144             return this.pointAtDistance(d);
145         }
146     }
147 }