Program Listing for File geodesics.h

Return to documentation for file (include/geodesy/geodesics.h)

/*********************************************************************
* Software License Agreement (BSD License)
*
*  Copyright (c) 2025 Roland Arsenault
*  All rights reserved.
*
*  Redistribution and use in source and binary forms, with or without
*  modification, are permitted provided that the following conditions
*  are met:
*
*   * Redistributions of source code must retain the above copyright
*     notice, this list of conditions and the following disclaimer.
*   * Redistributions in binary form must reproduce the above
*     copyright notice, this list of conditions and the following
*     disclaimer in the documentation and/or other materials provided
*     with the distribution.
*   * Neither the name of the author nor other contributors may be
*     used to endorse or promote products derived from this software
*     without specific prior written permission.
*
*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
*  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
*  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
*  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
*  COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
*  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
*  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
*  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
*  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
*  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
*  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
*  POSSIBILITY OF SUCH DAMAGE.
*********************************************************************/

#ifndef GEODESY__GEODESICS_H_
#define GEODESY__GEODESICS_H_


#include <cmath>

#include "geodesy/wgs84.h"
#include "geodesy/wgs84_ellipsoid.h"
#include "geographic_msgs/msg/geo_point.hpp"
#include "geometry_msgs/msg/vector3.hpp"

namespace geodesy
{

// Implementation of direct and inverse geodesic calculations using Vincenty's formulae.
//
// Vincenty, Thaddeus (April 1975a). "Direct and Inverse Solutions of Geodesics on the
// Ellipsoid with application of nested equations". Survey Review. XXIII (176): 88–93.
// https://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf
//
// Vincenty, Thaddeus (August 1975b). Geodetic inverse solution between antipodal
// points (Technical report). DMAAC Geodetic Survey Squadron. doi:10.5281/zenodo.32999
// https://geographiclib.sourceforge.io/geodesic-papers/vincenty75b.pdf


struct AzimuthDistance
{
  double azimuth;
  double distance;
};

template < typename EllipsoidParameters >
static geographic_msgs::msg::GeoPoint direct(
  const geographic_msgs::msg::GeoPoint & p1,
  double azimuth, double distance)
{
  // Implemented using Vincenty's formulae.
  // https://en.wikipedia.org/wiki/Vincenty%27s_formulae

  if(!is2D(p1) && p1.altitude != 0.0) {
    throw std::invalid_argument(
      "Altitude must be zero or not specified for direct geodesic calculations.");
  }

  // Convert to radians
  double phi1 = p1.latitude * M_PI / 180.0;
  double alpha1 = azimuth;

  // Vincenty uses an epsilon of 1e-12.
  double epsilon = 1e-12;

  // U is 'reduced latitude'
  double tanU1 = (1.0 - EllipsoidParameters::f) * tan(phi1);
  double cosU1 = 1 / sqrt(1 + tanU1 * tanU1);
  double sinU1 = tanU1 * cosU1;

  double cosAlpha1 = cos(alpha1);
  double sinAlpha1 = sin(alpha1);

  // angular distance on sphere from equator to P1 along geodesic
  double sigma1 = atan2(tanU1, cosAlpha1);

  // Eq (2)
  double sinAlpha = cosU1 * sinAlpha1;
  double cos2Alpha = 1.0 - sinAlpha * sinAlpha;

  double a = EllipsoidParameters::a;
  double b = EllipsoidParameters::b;
  double f = EllipsoidParameters::f;

  double u2 = cos2Alpha * (a * a - b * b) / (b * b);

  double k1 = (sqrt(1.0 + u2) - 1.0) / (sqrt(1.0 + u2) + 1.0);

  // Using Vincenty's 1976 modification
  // Replaces Eq (3)
  double A = (1.0 + k1 * k1 / 4.0) / (1.0 - k1);
  // Replaces Eq (4)
  double B = k1 * (1.0 - 3.0 * k1 * k1 / 8.0);

  // Angular distance from P1 to P2 on the sphere
  double sigma = distance / (b * A);
  double last_sigma;
  double cos2Sigmam;

  uint16_t iteration_count = 0;
  while (true) {
    iteration_count++;
    if (iteration_count > 10) {
      throw std::runtime_error("Geodesic direct formula did not converge after 1000 iterations.");
    }
    // cosine of Eq (5)
    cos2Sigmam = cos(2.0 * sigma1 + sigma);

    double sinSigma = sin(sigma);

    double cosSigma = cos(sigma);

    // Eq (6)
    double deltaSigma = B * sinSigma *
      (cos2Sigmam + 0.25 * B * (cosSigma * (-1.0 + 2.0 * cos2Sigmam * cos2Sigmam) -
      (B / 6.0) * cos2Sigmam * (-3.0 + 4.0 * sinSigma * sinSigma) *
      (-3.0 + 4.0 * cos2Sigmam * cos2Sigmam)
      )
      );

    last_sigma = sigma;

    // Eq (7)
    sigma = (distance / (b * A)) + deltaSigma;

    if (fabs(last_sigma - sigma) <= epsilon) {
      break;
    }
  }
  cos2Sigmam = cos(2.0 * sigma1 + sigma);

  // arctangent of Eq (8)
  double phi2 = atan2(sinU1 * cos(sigma) + cosU1 * sin(sigma) * cosAlpha1,
      (1 - f) * sqrt(
        sinAlpha * sinAlpha + pow(sinU1 * sin(sigma) - cosU1 * cos(sigma) * cosAlpha1, 2)
  ));

  // arctangent of Eq (9)
  double l = atan2(sin(sigma) * sinAlpha1, cosU1 * cos(sigma) - sinU1 * sin(sigma) * cosAlpha1);

  // Eq (10)
  double C = (f / 16.0) * cos2Alpha * (4.0 + f * (4.0 - 3.0 * cos2Alpha));

  // Eq (11)
  double L = l - (1.0 - C) * f * sinAlpha *
    (sigma + C * sin(sigma) *
    (cos2Sigmam + C * cos(sigma) * (-1 + 2.0 * cos2Sigmam * cos2Sigmam)));

  if(L < -M_PI) {
    L += 2 * M_PI;
  } else if(L > M_PI) {
    L -= 2 * M_PI;
  }

  // Convert back to degrees
  geographic_msgs::msg::GeoPoint result;
  result.latitude = phi2 * 180.0 / M_PI;
  result.longitude = p1.longitude + (L * 180.0 / M_PI);
  result.altitude = p1.altitude;  // Keep the same altitude

  return result;
}

template < typename EllipsoidParameters >
static geographic_msgs::msg::GeoPoint direct(
  const geographic_msgs::msg::GeoPoint & p1,
  AzimuthDistance azimuth_distance)
{
  return direct < EllipsoidParameters > (p1, azimuth_distance.azimuth, azimuth_distance.distance);
}

template < typename EllipsoidParameters >
static geographic_msgs::msg::GeoPoint direct(
  const geographic_msgs::msg::GeoPoint & p1,
  const geometry_msgs::msg::Vector3 & direction)
{
  if(direction.z != 0.0 || !std::isnan(direction.z)) {
    throw std::invalid_argument(
      "Direction vector must have z component zero or not specified for direct geodesic "
      "calculations.");
  }

  double azimuth = (M_PI / 2.0) - atan2(direction.y, direction.x);
  double distance = sqrt(direction.x * direction.x + direction.y * direction.y);

  return direct < EllipsoidParameters > (p1, azimuth, distance);
}


template < typename EllipsoidParameters >
static AzimuthDistance inverse(
  const geographic_msgs::msg::GeoPoint & p1,
  const geographic_msgs::msg::GeoPoint & p2)
{
  // Implemented using Vincenty's formulae.
  // https://en.wikipedia.org/wiki/Vincenty%27s_formulae

  if(!is2D(p1) && p1.altitude != 0.0 || !is2D(p2) && p2.altitude != 0.0) {
    throw std::invalid_argument(
      "Altitude must be zero or not specified for inverse geodesic calculations.");
  }

  if(p1.latitude == p2.latitude && p1.longitude == p2.longitude) {
    return {0.0, 0.0};  // Same point, no azimuth or distance
  }

  double a = EllipsoidParameters::a;
  double b = EllipsoidParameters::b;
  double f = EllipsoidParameters::f;

  double epsilon = 1e-12;

  double phi1 = p1.latitude * M_PI / 180.0;
  double phi2 = p2.latitude * M_PI / 180.0;

  // difference in longitude
  double L = (p2.longitude - p1.longitude) * M_PI / 180.0;

  // reduced latitudes
  double U1 = atan((1.0 - f) * tan(phi1));
  double U2 = atan((1.0 - f) * tan(phi2));

  double cosU1 = cos(U1);
  double cosU2 = cos(U2);
  double sinU1 = sin(U1);
  double sinU2 = sin(U2);

  // Eq (13), difference in longitude on auxiliary sphere
  double lambda = L;

  double sinSigma;
  double cosSigma;

  // angular distance on the sphere
  double sigma;

  double cos2Alpha;
  double sinAlpha;
  double cos2Sigmam;
  double alpha1 = std::nan("");
  double A = std::nan("");
  double B = std::nan("");

  double last_lambda = std::nan("");
  double cosLambda;
  double sinLambda;

  uint16_t iteration_count = 0;

  while (true) {
    iteration_count++;
    if (iteration_count > 200) {
      throw std::runtime_error("Geodesic inverse formula did not converge after 200 iterations.");
    }


    cosLambda = cos(lambda);
    sinLambda = sin(lambda);

    // Square root of Eq (14)
    sinSigma = sqrt((pow((cosU2 * sinLambda),
      2)) + pow((cosU1 * sinU2 - sinU1 * cosU2 * cosLambda), 2));

    // Eq (15)
    cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;

    // Arctangent of Eq (16), angular distance P1 to P2 on the sphere
    sigma = atan2(sinSigma, cosSigma);

    // Eq (17)
    sinAlpha = (cosU1 * cosU2 * sinLambda) / sinSigma;

    cos2Alpha = 1 - sinAlpha * sinAlpha;

    // Eq (10)
    double C = (f / 16.0) * cos2Alpha * (4.0 + f * (4.0 - 3.0 * cos2Alpha));

    if (cos2Alpha == 0) {
      cos2Sigmam = 0;
    } else {
      // Eq (18)
      cos2Sigmam = cosSigma - ((2.0 * sinU1 * sinU2) / cos2Alpha);
    }


    if (!std::isnan(last_lambda) && fabs(last_lambda - lambda) <= epsilon) {
      break;
    }
    last_lambda = lambda;

    // reverse of Eq (11)
    lambda = L + (1.0 - C) * f * sinAlpha *
      (sigma + C * sinSigma *
      (cos2Sigmam + C * cosSigma * (-1.0 + 2.0 * cos2Sigmam * cos2Sigmam)));

    if(fabs(lambda) > M_PI) {
      throw std::runtime_error(
        "Geodesic inverse formula failed: lambda > π (Antipodal case not handled)");
    }
  }

  double u2 = cos2Alpha * (a * a - b * b) / (b * b);
  // Using Vincenty's 1976 modification
  double k1 = (sqrt(1.0 + u2) - 1.0) / (sqrt(1.0 + u2) + 1.0);
  A = (1.0 + k1 * k1 / 4.0) / (1.0 - k1);
  B = k1 * (1.0 - 3.0 * k1 * k1 / 8.0);

  // Arctangent of Eq (20)
  alpha1 = atan2(cosU2 * sinLambda, cosU1 * sinU2 - sinU1 * cosU2 * cosLambda);

  // 1975a Eq (6)
  double deltaSigma = B * sinSigma *
    (cos2Sigmam + 0.25 * B *
    (cosSigma * (-1.0 + 2.0 * cos2Sigmam * cos2Sigmam) - (B / 6.0) * cos2Sigmam *
    (-3.0 + 4.0 * sinSigma * sinSigma) * (-3.0 + 4.0 * cos2Sigmam * cos2Sigmam)));

  // 1975a Eq (19)
  double s = b * A * (sigma - deltaSigma);

  double azimuth = fmod((alpha1 + 2 * M_PI), (2 * M_PI));  // Normalize to [0, 2π)

  AzimuthDistance result;
  result.azimuth = azimuth;  // Azimuth in radians, clockwise from north
  result.distance = s;  // Distance in meters
  return result;
}

template < typename EllipsoidParameters >
static geometry_msgs::msg::Vector3 inverse_vector(
  const geographic_msgs::msg::GeoPoint & p1,
  const geographic_msgs::msg::GeoPoint & p2)
{
  AzimuthDistance ad = inverse < EllipsoidParameters > (p1, p2);
  double azimuth = (M_PI / 2.0) - ad.azimuth;  // Convert azimuth to east-north coordinate system
  geometry_msgs::msg::Vector3 result;
  result.x = ad.distance * cos(azimuth);  // East component
  result.y = ad.distance * sin(azimuth);  // North component
  result.z = 0.0;  // Z component is zero for horizontal direction
  return result;
}

namespace wgs84
{
  static inline geographic_msgs::msg::GeoPoint direct(
    const geographic_msgs::msg::GeoPoint & p1,
    double azimuth, double distance)
  {
    return geodesy::direct < wgs84::Ellipsoid::Parameters > (p1, azimuth, distance);
  }

  static inline AzimuthDistance inverse(
    const geographic_msgs::msg::GeoPoint & p1,
    const geographic_msgs::msg::GeoPoint & p2)
  {
    return geodesy::inverse < wgs84::Ellipsoid::Parameters > (p1, p2);
  }


  static geometry_msgs::msg::Vector3 inverse_vector(
    const geographic_msgs::msg::GeoPoint & p1,
    const geographic_msgs::msg::GeoPoint & p2)
  {
    return geodesy::inverse_vector < wgs84::Ellipsoid::Parameters > (p1, p2);
  }

}  // namespace wgs84

}  // namespace geodesy


#endif  // GEODESY__GEODESICS_H_