Loxodrome.java
/* Copyright 2022 Joseph Reed
* Licensed to CS GROUP (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* Joseph Reed licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.bodies;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.MathUtils;
/** Perform calculations on a loxodrome (commonly, a rhumb line) on an ellipsoid.
* <p>
* A <a href="https://en.wikipedia.org/wiki/Rhumb_line">loxodrome or rhumb line</a>
* is an arc on an ellipsoid's surface that intersects every meridian at the same angle.
*
* @author Joe Reed
* @since 11.3
*/
public class Loxodrome {
/** Threshold for cos angles being equal. */
private static final double COS_ANGLE_THRESHOLD = 1e-6;
/** Threshold for when distances are close enough to zero. */
private static final double DISTANCE_THRESHOLD = 1e-9;
/** Reference point for the loxodrome. */
private final GeodeticPoint point;
/** Azimuth-off-north angle of the loxodrome. */
private final double azimuth;
/** Reference body. */
private final OneAxisEllipsoid body;
/** Altitude above the body. */
private final double altitude;
/** Constructor building a loxodrome from an initial point and an azimuth-off-local-north heading.
*
* This method is an equivalent to {@code new Loxodrome(point, azimuth, body, point.getAltitude())}
*
* @param point the initial loxodrome point
* @param azimuth the heading, clockwise angle from north (radians, {@code [0,2π]})
* @param body ellipsoid body on which the loxodrome is defined
*/
public Loxodrome(final GeodeticPoint point, final double azimuth, final OneAxisEllipsoid body) {
this(point, azimuth, body, point.getAltitude());
}
/** Constructor building a loxodrome from an initial point and an azimuth-off-local-north heading.
*
* @param point the initial loxodrome point
* @param azimuth the heading, clockwise angle from north (radians, {@code [0,2π]})
* @param body ellipsoid body on which the loxodrome is defined
* @param altitude altitude above the reference body
*/
public Loxodrome(final GeodeticPoint point, final double azimuth, final OneAxisEllipsoid body,
final double altitude) {
this.point = point;
this.azimuth = azimuth;
this.body = body;
this.altitude = altitude;
}
/** Get the geodetic point defining the loxodrome.
* @return the geodetic point defining the loxodrome
*/
public GeodeticPoint getPoint() {
return this.point;
}
/** Get the azimuth.
* @return the azimuth
*/
public double getAzimuth() {
return this.azimuth;
}
/** Get the body on which the loxodrome is defined.
* @return the body on which the loxodrome is defined
*/
public OneAxisEllipsoid getBody() {
return this.body;
}
/** Get the altitude above the reference body.
* @return the altitude above the reference body
*/
public double getAltitude() {
return this.altitude;
}
/** Calculate the point at the specified distance from the origin point along the loxodrome.
*
* A positive distance follows the line in the azimuth direction (i.e. northward for arcs with azimuth
* angles {@code [3π/2, 2π]} or {@code [0, π/2]}). Negative distances travel in the opposite direction along
* the rhumb line.
*
* Distance is computed at the altitude of the origin point.
*
* @param distance the distance to travel (meters)
* @return the point at the specified distance from the origin
*/
public GeodeticPoint pointAtDistance(final double distance) {
if (FastMath.abs(distance) < DISTANCE_THRESHOLD) {
return this.point;
}
// compute the e sin(lat)^2
final double sinLat = FastMath.sin(point.getLatitude());
final double eccSinLatSq = body.getEccentricitySquared() * sinLat * sinLat;
// compute intermediate values
final double t1 = 1. - body.getEccentricitySquared();
final double t2 = 1. - eccSinLatSq;
final double t3 = FastMath.sqrt(t2);
final double semiMajorAxis = getBody().getEquatorialRadius() + getAltitude();
final double meridianCurve = (semiMajorAxis * t1) / (t2 * t3);
final double cosAzimuth = FastMath.cos(azimuth);
final double lat;
final double lon;
if (FastMath.abs(cosAzimuth) < COS_ANGLE_THRESHOLD) {
lat = point.getLatitude();
lon = point.getLongitude() + ((distance * FastMath.sin(azimuth) * t3) / (semiMajorAxis * FastMath.cos(point.getLatitude())));
}
else {
final double eccSq34 = 0.75 * body.getEccentricitySquared();
final double halfEccSq34 = eccSq34 / 2.;
final double t4 = meridianCurve / (t1 * semiMajorAxis);
final double latPrime = point.getLatitude() + distance * cosAzimuth / meridianCurve;
final double latOffset = t4 * (
((1. - eccSq34) * (latPrime - point.getLatitude())) +
(halfEccSq34 * (FastMath.sin(2. * latPrime) - FastMath.sin(2. * point.getLatitude()))));
lat = fixLatitude(point.getLatitude() + latOffset);
final double lonOffset = FastMath.tan(azimuth) * (body.geodeticToIsometricLatitude(lat) - body.geodeticToIsometricLatitude(point.getLatitude()));
lon = point.getLongitude() + lonOffset;
}
return new GeodeticPoint(lat, lon, getAltitude());
}
/** Adjust the latitude if necessary, ensuring it's always between -π/2 and +π/2.
*
* @param lat the latitude value
* @return the latitude, within {@code [-π/2,+π/2]}
*/
static double fixLatitude(final double lat) {
if (lat < -MathUtils.SEMI_PI) {
return -MathUtils.SEMI_PI;
}
else if (lat > MathUtils.SEMI_PI) {
return MathUtils.SEMI_PI;
}
else {
return lat;
}
}
}