Почему расстояние boost::geometry geographic Vincenty неточно на экваторе?

Мне нужна функция для расчета расстояния между парой WGS 84 с высокой степенью точности, и я планировал использовать geographic функции из усилить геометрию.

геометрия Boost Design Rational состояния:

Есть метод Андуайе, быстрый и точный, и есть метод Винсенти, более медленный и точный.

Однако при тестировании функции boost::geometry::distance со стратегиями Andoyer и Vincenty я получил следующие результаты:

WGS 84 values (metres)
    Semimajor axis:         6378137.000000
    Flattening:             0.003353
    Semiminor axis:         6356752.314245

    Semimajor distance:     20037508.342789
    Semiminor distance:     19970326.371123

Boost geometry near poles
Andoyer function:
    Semimajor distance:     20037508.151445
    Semiminor distance:     20003917.164970
Vincenty function:
    Semimajor distance:     **19970326.180419**
    Semiminor distance:     20003931.266635

Boost geometry at poles
Andoyer function:
    Semimajor distance:     0.000000
    Semiminor distance:     0.000000
Vincenty function:
    Semimajor distance:     **19970326.371122**
    Semiminor distance:     20003931.458623

Vincenty расстояний по большой полуоси (т.е. вокруг экватора) меньше, чем расстояние по малой полуоси между Северным и Южным полюсами. Это не может быть правильным.

Полуминорное и Andoyer расстояния выглядят разумными. За исключением случаев, когда точки находятся на противоположной стороне Земли, когда функция boost Andoyer возвращает ноль!

В чем проблема: в алгоритме Vincenty, в его реализации boost geometry или в моем тестовом коде?

Тестовый код:

/// boost geometry WGS84 distance issue

// Note: M_PI is not part of the C or C++ standards, _USE_MATH_DEFINES enables it
#define _USE_MATH_DEFINES
#include <boost/geometry.hpp>
#include <cmath>
#include <iostream>
#include <ios>

// WGS 84 parameters from: Eurocontrol WGS 84 Implementation Manual
// Version 2.4 Chapter 3, page 14

/// The Semimajor axis measured in metres.
/// This is the radius at the equator.
constexpr double a = 6378137.0;

/// Flattening, a ratio.
/// This is the flattening of the ellipse at the poles
constexpr double f = 1.0/298.257223563;

/// The Semiminor axis measured in metres.
/// This is the radius at the poles.
/// Note: this is derived from the Semimajor axis and the flattening.
/// See WGS 84 Implementation Manual equation B-2, page 69.
constexpr double b = a * (1.0 - f);

int main(int /*argc*/, char ** /*argv*/)
{
  std::cout.setf(std::ios::fixed);

  std::cout << "WGS 84 values (metres)\n";
  std::cout << "\tSemimajor axis:\t\t"   << a << "\n";
  std::cout << "\tFlattening:\t\t"       << f << "\n";
  std::cout << "\tSemiminor axis:\t\t"   << b << "\n\n";

  std::cout << "\tSemimajor distance:\t" << M_PI * a << "\n";
  std::cout << "\tSemiminor distance:\t" << M_PI * b << "\n";
  std::cout << std::endl;

  // Min value for delta. 0.000000014 causes Andoyer to fail.
  const double DELTA(0.000000015);

  // For boost::geometry:
  typedef boost::geometry::cs::geographic<boost::geometry::radian> Wgs84Coords;
  typedef boost::geometry::model::point<double, 2, Wgs84Coords> GeographicPoint;
  // Note boost points are Long & Lat NOT Lat & Long
  GeographicPoint near_north_pole   (0.0,  M_PI_2 - DELTA);
  GeographicPoint near_south_pole   (0.0, -M_PI_2 + DELTA);

  GeographicPoint near_equator_east ( M_PI_2 - DELTA, 0.0);
  GeographicPoint near_equator_west (-M_PI_2 + DELTA, 0.0);

  // Note: the default boost geometry spheroid is WGS84
  // #include <boost/geometry/core/srs.hpp>
  typedef boost::geometry::srs::spheroid<double> SpheroidType;
  SpheroidType spheriod;

  //#include <boost/geometry/strategies/geographic/distance_andoyer.hpp>
  typedef boost::geometry::strategy::distance::andoyer<SpheroidType>
                                                               AndoyerStrategy;
  AndoyerStrategy andoyer(spheriod);

  std::cout << "Boost geometry near poles\n";
  std::cout << "Andoyer function:\n";
  double andoyer_major(boost::geometry::distance(near_equator_east, near_equator_west, andoyer));
  std::cout << "\tSemimajor distance:\t" << andoyer_major << "\n";
  double andoyer_minor(boost::geometry::distance(near_north_pole, near_south_pole, andoyer));
  std::cout << "\tSemiminor distance:\t" << andoyer_minor << "\n";

  //#include <boost/geometry/strategies/geographic/distance_vincenty.hpp>
  typedef boost::geometry::strategy::distance::vincenty<SpheroidType>
                                                               VincentyStrategy;
  VincentyStrategy vincenty(spheriod);

  std::cout << "Vincenty function:\n";
  double vincenty_major(boost::geometry::distance(near_equator_east, near_equator_west, vincenty));
  std::cout << "\tSemimajor distance:\t" << vincenty_major << "\n";
  double vincenty_minor(boost::geometry::distance(near_north_pole, near_south_pole, vincenty));
  std::cout << "\tSemiminor distance:\t" << vincenty_minor << "\n\n";

  // Note boost points are Long & Lat NOT Lat & Long
  GeographicPoint north_pole   (0.0,  M_PI_2);
  GeographicPoint south_pole   (0.0, -M_PI_2);

  GeographicPoint equator_east ( M_PI_2, 0.0);
  GeographicPoint equator_west (-M_PI_2, 0.0);

  std::cout << "Boost geometry at poles\n";
  std::cout << "Andoyer function:\n";
  andoyer_major = boost::geometry::distance(equator_east, equator_west, andoyer);
  std::cout << "\tSemimajor distance:\t" << andoyer_major << "\n";
  andoyer_minor = boost::geometry::distance(north_pole, south_pole, andoyer);
  std::cout << "\tSemiminor distance:\t" << andoyer_minor << "\n";

  std::cout << "Vincenty function:\n";
  vincenty_major = boost::geometry::distance(equator_east, equator_west, vincenty);
  std::cout << "\tSemimajor distance:\t" << vincenty_major << "\n";
  vincenty_minor = boost::geometry::distance(north_pole, south_pole, vincenty);
  std::cout << "\tSemiminor distance:\t" << vincenty_minor << "\n";

  return 0;
}

person kenba    schedule 13.01.2016    source источник


Ответы (2)


В качестве альтернативы посмотрите geographiclib Чарльза Ф. Ф. Карни. Как сказано в документации: «Упор делается на получение точных результатов с ошибками, близкими к округлению (около 5–15 нанометров)».

person jwd630    schedule 13.01.2016
comment
Спасибо @jwd630, я сейчас читаю, что Charles F. F. Karney's [Algorithms for geodesics](http://link.springer.com/article/10.1007%2Fs00190-012-0578-z) paper and geographiclib would be an excelent alternative. However, boost геометрия` - это boost библиотека, то есть практически стандартная, поэтому она должна работать правильно. - person kenba; 13.01.2016
comment
Извините, @jwd630, но geographiclib хуже! Смотрите мой ответ... - person kenba; 17.01.2016
comment
@cffk действительно так, и «Андуайер», и Vincenty неверны. Извините моя ошибка. - person kenba; 17.01.2016

Я последовал совету @jwd630 и проверил geographiclib.
Вот результаты:

WGS 84 values (metres)
    Semimajor distance:    20037508.342789
    Semiminor distance:    19970326.371123

GeographicLib near antipodal
    Semimajor distance:    20003931.458625
    Semiminor distance:    20003931.455275

GeographicLib antipodal
    Semimajor distance:    20003931.458625
    Semiminor distance:    20003931.458625

GeographicLib verify
    JFK to LHR distance:   5551759.400319

т.е. он обеспечивает то же расстояние, что и Vincenty, для малого полуминорного расстояния между полюсами (до 5 dp) и вычисляет такое же расстояние для противоположных точек на экваторе.

Это правильно, поскольку кратчайшее расстояние между противоположными точками на экваторе проходит через один из полюсов, а не вокруг экватора, как рассчитывает алгоритм повышения Andoyer по умолчанию.

Таким образом, приведенный выше ответ @jwd630 является правильным, и из трех алгоритмов geographiclib является единственным, который вычисляет правильное расстояние по весь геоид WGS84.

Вот тестовый код:

/// GeographicLib  WGS84 distance

// Note: M_PI is not part of the C or C++ standards, _USE_MATH_DEFINES enables it
#define _USE_MATH_DEFINES
#include <GeographicLib/Geodesic.hpp>
#include <cmath>
#include <iostream>
#include <ios>

// WGS 84 parameters from: Eurocontrol WGS 84 Implementation Manual
// Version 2.4 Chapter 3, page 14

/// The Semimajor axis measured in metres.
/// This is the radius at the equator.
constexpr double a = 6378137.0;

/// Flattening, a ratio.
/// This is the flattening of the ellipse at the poles
constexpr double f = 1.0/298.257223563;

/// The Semiminor axis measured in metres.
/// This is the radius at the poles.
/// Note: this is derived from the Semimajor axis and the flattening.
/// See WGS 84 Implementation Manual equation B-2, page 69.
constexpr double b = a * (1.0 - f);

int main(int /*argc*/, char ** /*argv*/)
{
  const GeographicLib::Geodesic& geod(GeographicLib::Geodesic::WGS84());

  std::cout.setf(std::ios::fixed);

  std::cout << "WGS 84 values (metres)\n";
  std::cout << "\tSemimajor axis:\t\t"   << a << "\n";
  std::cout << "\tFlattening:\t\t"       << f << "\n";
  std::cout << "\tSemiminor axis:\t\t"   << b << "\n\n";

  std::cout << "\tSemimajor distance:\t" << M_PI * a << "\n";
  std::cout << "\tSemiminor distance:\t" << M_PI * b << "\n";
  std::cout << std::endl;

  // Min value for delta. 0.000000014 causes boost Andoyer to fail.
  const double DELTA(0.000000015);

  std::pair<double, double> near_equator_east (0.0, 90.0 - DELTA);
  std::pair<double, double> near_equator_west (0.0, -90.0 + DELTA);

  std::cout << "GeographicLib near antipodal\n";
  double distance_metres(0.0);
  geod.Inverse(near_equator_east.first, near_equator_east.second,
               near_equator_west.first, near_equator_west.second, distance_metres);
  std::cout << "\tSemimajor distance:\t" << distance_metres << "\n";

  std::pair<double, double> near_north_pole   (90.0 - DELTA, 0.0);
  std::pair<double, double> near_south_pole   (-90.0 + DELTA, 0.0);

  geod.Inverse(near_north_pole.first, near_north_pole.second,
               near_south_pole.first, near_south_pole.second, distance_metres);
  std::cout << "\tSemiminor distance:\t" << distance_metres << "\n\n";

  std::pair<double, double> equator_east (0.0, 90.0);
  std::pair<double, double> equator_west (0.0, -90.0);

  std::cout << "GeographicLib antipodal\n";
  geod.Inverse(equator_east.first, equator_east.second,
               equator_west.first, equator_west.second, distance_metres);
  std::cout << "\tSemimajor distance:\t" << distance_metres << "\n";

  std::pair<double, double> north_pole   (90.0, 0.0);
  std::pair<double, double> south_pole   (-90.0, 0.0);

  geod.Inverse(north_pole.first, north_pole.second,
               south_pole.first, south_pole.second, distance_metres);
  std::cout << "\tSemiminor distance:\t" << distance_metres << "\n\n";

  std::pair<double, double> JFK   (40.6, -73.8);
  std::pair<double, double> LHR   (51.6, -0.5);

  std::cout << "GeographicLib verify distance\n";
  geod.Inverse(JFK.first, JFK.second,
               LHR.first, LHR.second, distance_metres);
  std::cout << "\tJFK to LHR distance:\t" << distance_metres << std::endl;

  return 0;
}

В своей статье Алгоритмы для геодезических Чарльз Ф. Ф. Карни утверждает, что " Метод Винсенти не сходится для почти противоположных точек». Что может ответить на мой первоначальный вопрос, то есть алгоритм Vincenty не подходит для противоположных точек.

Примечание. Я поднял boost тикет #11817 с описанием проблемы, из-за которой Andoyer алгоритм возвращает ноль для противоположных точек и отправляет запрос на извлечение на boost с исправлением для него.

Однако единственно правильное исправление неправильных расстояний — это использование правильного алгоритма, а именно: geographiclib

Большое спасибо Charles F. F. Karney (@cffk) за вежливое указание на мои глупые ошибки!

person kenba    schedule 17.01.2016
comment
У вас неправильное положение южного полюса! Это широта = -90, а не широта = 90. - person cffk; 17.01.2016
comment
@cffk примите мои самые искренние извинения. Вы совершенно правы, у меня была широта Южного полюса на Северном полюсе. Так что ноль был правильным, и я не ожидал, что многие алгоритмы справятся с Latitudes › 90 градусов! Я изменил тестовый код и отредактировал ответ с новыми (надеюсь, правильными) результатами. Расстояние между полюсами теперь выглядит правильно, но наверняка расстояние вокруг экватора должно быть Пи * а, то есть 20037508,342789 метров? - person kenba; 17.01.2016
comment
Нет, для сплюснутого эллипсоида короче пройти через полюс. Кратчайшая геодезическая линия между двумя точками экватора проходит по экватору только в том случае, если разница долготы составляет (1 - f) 180° или меньше. (Что касается широт > 90 °, GeographicLib намеренно преобразует их в NaN, чтобы предотвратить возврат правдоподобных, но неправильных результатов.) - person cffk; 17.01.2016
comment
Спасибо @cffk, конечно, кратчайшее расстояние через один из столбов, о чем я думал? Дох! Я снова изменил свой ответ и принял ответ jwd630. - person kenba; 17.01.2016