Loxodrome.java

  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. import org.hipparchus.util.FastMath;
  19. import org.hipparchus.util.MathUtils;

  20. /** Perform calculations on a loxodrome (commonly, a rhumb line) on an ellipsoid.
  21.  * <p>
  22.  * A <a href="https://en.wikipedia.org/wiki/Rhumb_line">loxodrome or rhumb line</a>
  23.  * is an arc on an ellipsoid's surface that intersects every meridian at the same angle.
  24.  *
  25.  * @author Joe Reed
  26.  * @since 11.3
  27.  */
  28. public class Loxodrome {

  29.     /** Threshold for cos angles being equal. */
  30.     private static final double COS_ANGLE_THRESHOLD = 1e-6;

  31.     /** Threshold for when distances are close enough to zero. */
  32.     private static final double DISTANCE_THRESHOLD = 1e-9;

  33.     /** Reference point for the loxodrome. */
  34.     private final GeodeticPoint point;

  35.     /** Azimuth-off-north angle of the loxodrome. */
  36.     private final double azimuth;

  37.     /** Reference body. */
  38.     private final OneAxisEllipsoid body;

  39.     /** Altitude above the body. */
  40.     private final double altitude;

  41.     /** Constructor building a loxodrome from an initial point and an azimuth-off-local-north heading.
  42.      *
  43.      * This method is an equivalent to {@code new Loxodrome(point, azimuth, body, point.getAltitude())}
  44.      *
  45.      * @param point the initial loxodrome point
  46.      * @param azimuth the heading, clockwise angle from north (radians, {@code [0,2π]})
  47.      * @param body ellipsoid body on which the loxodrome is defined
  48.      */
  49.     public Loxodrome(final GeodeticPoint point, final double azimuth, final OneAxisEllipsoid body) {
  50.         this(point, azimuth, body, point.getAltitude());
  51.     }

  52.     /** Constructor building a loxodrome from an initial point and an azimuth-off-local-north heading.
  53.      *
  54.      * @param point the initial loxodrome point
  55.      * @param azimuth the heading, clockwise angle from north (radians, {@code [0,2π]})
  56.      * @param body ellipsoid body on which the loxodrome is defined
  57.      * @param altitude altitude above the reference body
  58.      */
  59.     public Loxodrome(final GeodeticPoint point, final double azimuth, final OneAxisEllipsoid body,
  60.                      final double altitude) {
  61.         this.point    = point;
  62.         this.azimuth  = azimuth;
  63.         this.body     = body;
  64.         this.altitude = altitude;
  65.     }

  66.     /** Get the geodetic point defining the loxodrome.
  67.      * @return the geodetic point defining the loxodrome
  68.      */
  69.     public GeodeticPoint getPoint() {
  70.         return this.point;
  71.     }

  72.     /** Get the azimuth.
  73.      * @return the azimuth
  74.      */
  75.     public double getAzimuth() {
  76.         return this.azimuth;
  77.     }

  78.     /** Get the body on which the loxodrome is defined.
  79.      * @return the body on which the loxodrome is defined
  80.      */
  81.     public OneAxisEllipsoid getBody() {
  82.         return this.body;
  83.     }

  84.     /** Get the altitude above the reference body.
  85.      * @return the altitude above the reference body
  86.      */
  87.     public double getAltitude() {
  88.         return this.altitude;
  89.     }

  90.     /** Calculate the point at the specified distance from the origin point along the loxodrome.
  91.      *
  92.      * A positive distance follows the line in the azimuth direction (i.e. northward for arcs with azimuth
  93.      * angles {@code [3π/2, 2π]} or {@code [0, π/2]}). Negative distances travel in the opposite direction along
  94.      * the rhumb line.
  95.      *
  96.      * Distance is computed at the altitude of the origin point.
  97.      *
  98.      * @param distance the distance to travel (meters)
  99.      * @return the point at the specified distance from the origin
  100.      */
  101.     public GeodeticPoint pointAtDistance(final double distance) {
  102.         if (FastMath.abs(distance) < DISTANCE_THRESHOLD) {
  103.             return this.point;
  104.         }

  105.         // compute the e sin(lat)^2
  106.         final double sinLat = FastMath.sin(point.getLatitude());
  107.         final double eccSinLatSq = body.getEccentricitySquared() * sinLat * sinLat;

  108.         // compute intermediate values
  109.         final double t1 = 1. - body.getEccentricitySquared();
  110.         final double t2 = 1. - eccSinLatSq;
  111.         final double t3 = FastMath.sqrt(t2);

  112.         final double semiMajorAxis = getBody().getEquatorialRadius() + getAltitude();

  113.         final double meridianCurve = (semiMajorAxis * t1) / (t2 * t3);

  114.         final double cosAzimuth = FastMath.cos(azimuth);

  115.         final double lat;
  116.         final double lon;
  117.         if (FastMath.abs(cosAzimuth) < COS_ANGLE_THRESHOLD) {
  118.             lat = point.getLatitude();
  119.             lon = point.getLongitude() + ((distance * FastMath.sin(azimuth) * t3) / (semiMajorAxis * FastMath.cos(point.getLatitude())));
  120.         }
  121.         else {
  122.             final double eccSq34 = 0.75 * body.getEccentricitySquared();
  123.             final double halfEccSq34 = eccSq34 / 2.;
  124.             final double t4 = meridianCurve / (t1 * semiMajorAxis);

  125.             final double latPrime = point.getLatitude() + distance * cosAzimuth / meridianCurve;
  126.             final double latOffset = t4 * (
  127.                 ((1. - eccSq34) * (latPrime - point.getLatitude())) +
  128.                         (halfEccSq34 * (FastMath.sin(2. * latPrime) - FastMath.sin(2. * point.getLatitude()))));

  129.             lat = fixLatitude(point.getLatitude() + latOffset);

  130.             final double lonOffset = FastMath.tan(azimuth) * (body.geodeticToIsometricLatitude(lat) - body.geodeticToIsometricLatitude(point.getLatitude()));
  131.             lon = point.getLongitude() + lonOffset;
  132.         }

  133.         return new GeodeticPoint(lat, lon, getAltitude());
  134.     }

  135.     /** Adjust the latitude if necessary, ensuring it's always between -π/2 and +π/2.
  136.      *
  137.      * @param lat the latitude value
  138.      * @return the latitude, within {@code [-π/2,+π/2]}
  139.      */
  140.     static double fixLatitude(final double lat) {
  141.         if (lat < -MathUtils.SEMI_PI) {
  142.             return -MathUtils.SEMI_PI;
  143.         }
  144.         else if (lat > MathUtils.SEMI_PI) {
  145.             return MathUtils.SEMI_PI;
  146.         }
  147.         else {
  148.             return lat;
  149.         }
  150.     }
  151. }