如何计算地球任意两点之间距离

相关源码和模型的下载链接地址点击链接进行跳转

前言

本文通过百度地图api实现一个地球地图模型,通过运用精确的Haversine公式Vincenty公式,系统能够迅速计算出这两个坐标点之间的实际距离。文中力求使用专业的地理和数学的术语解释计算原理,如有失误,请指正,谢谢

技术栈

  • typescript
  • 百度地图api
  • vite

加载地图

获取应用AK

需要实现加载百度地图提供的地图服务api,首先需要创建一个属于自己的应用AK,在百度地图开放平台--控制台--我的应用

创建地球

html 复制代码
// 引入文档
<script type="text/javascript" src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=你申请的应用AK"></script>

// 地图服务容器
<div id="allmap"></div>

在引入对应的map服务后,就该创建一个地图实例,

ts 复制代码
// GL版命名空间为BMapGL
var map_earth = new BMapGL.Map("allmap");    // 创建Map实例
map_earth.centerAndZoom(new BMapGL.Point(116.40396298757886, 39.91511908708907), 5);  // 初始化地图,设置中心点坐标和地图级别

当前center设置的是北京天安门的经纬坐标,缩放级别为5,当然,这创建的是普通的2维平面地图,我们要创建的是一个地球的效果,所以需要修改一下地图的风格

typescript 复制代码
map_earth.enableScrollWheelZoom(true);     //开启鼠标滚轮缩放
map_earth.setMapType(BMAP_EARTH_MAP);      // 设置地图类型为地球模式

这样我们就创建好了一个基础的地球地图模型

拾取经纬坐标

通过点击地球某个点位,拾取到经纬坐标,并记录这些坐标。

typescript 复制代码
// 点击获取经纬度
map_earth.addEventListener('click', function (e) {
    // 创建标注实例
    var marker = new BMapGL.Marker(new BMapGL.Point(e.latlng.lng, e.latlng.lat));
    // 添加标注
    map_earth.addOverlay(marker);
    // 获取地理信息
    getLocation(e.latlng)
})

获取地理信息

typescript 复制代码
const getLocation = (pt: { lng: number, lat: number }) => {
    geoc.getLocation(pt, function (rs) {
        console.log(rs);
    })
}

计算距离

Haversine公式

Haversine公式是一种基于球面三角学的数学公式,公式的主要作用就是提供两个经纬度,计算出大圆距离。但是地球并不是一个正球体,正因为曲率的原因,计算较远的距离时,会有一定的误差,但是对较短的距离相对比较精准。这里要解释一下什么是大圆,对于地球来说,大圆必须能经过过球心,将球体分为两半的圆,比如赤道圈,晨昏圈,经线圈和本初子午线,那么地球的最短弧长,必须是大圆的劣弧

Haversine公式有几个关键的变量,比如经度差和纬度差,然后通过这两个差值和两个地点的经纬度来计算一个中间值a,再根据中间值a通过反双曲正切函数计算出c的值,最后通过地球半径和中间值c计算两点之间的距离d,单位可以通过页面的单位选择器选择,根据选择的不同单位,计算半径,半径不同,计算结果也不同。

Haversine原公式

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> a = s i n 2 ( Δ l a t / 2 ) + c o s ( l a t 1 ) × c o s ( l a t 2 ) × s i n 2 ( Δ l o n / 2 ) c = 2 × a t a n 2 ( √ a , √ ( 1 − a ) ) d i s t a n c e = R × c a = sin²(Δlat/2) + cos(lat1) × cos(lat2) × sin²(Δlon/2) c = 2 × atan2(√a, √(1-a)) distance = R × c </math>a=sin2(Δlat/2)+cos(lat1)×cos(lat2)×sin2(Δlon/2)c=2×atan2(√a,√(1−a))distance=R×c

转化为js公式

typescript 复制代码
// 将纬度差转换为弧度  
const dLat = (lat2 - lat1) * (Math.PI / 180);
// 将经度差转换为弧度  
const dLon = (lgn2 - lgn1) * (Math.PI / 180);

// 使用Haversine公式计算a的值  
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) *
    Math.sin(dLon / 2) * Math.sin(dLon / 2);
    
// 通过反双曲正切函数c的值  
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

const mile = 0.621371192237
// 定义地球半径根据选择判定以哪个单位作为半径,半径单位不同,计算出的结果也不同
const R = 6371.004 * (unitVal?.value === 'mile' ? mile : 1);

 // 根据地球半径和c的值计算两点间的距离  
const distance = R * c;

Vincenty公式

如果说Haversine公式是基于球体数学模型计算的,那么Vincenty公式就是基于椭球体计算,将扁率作为计算参数,这里引用一个测绘学科的专业术语地球椭球体

众所周知我们的地球表面是一个凸凹不平的表面,而对于地球测量而言,地表是一个无法用数学公式表达的曲面,这样的曲面不能作为测量和制图的基准面。假想一个扁率极小的椭圆,绕大地球体短轴旋转所形成的规则椭球体称之为地球椭球体,此椭球体近似于大地水准面。地球椭球体表面是一个规则的数学表面,可以用数学公式表达,所以在测量和制图中就用它替代地球的自然表面。因此就有了地球椭球体的概念。一般在制图或测量中就用旋转椭球来代替大地球体,这个旋转球体通常称为地球椭球体,简称椭球体。
Vincenty公式通常使用WGS84椭球模型,这是GPS系统所使用的模型,也是目前最广泛使用的地球椭球模型之一

我从网上找到了地理信息系统(GIS)网站名为 Vincenty solutions of geodesics on the ellipsoid标题的文章,原文地址,上面详细介绍了如何计算出两个经纬度之间的距离的公式。也就是Vincenty公式。原文是英文版的,读起来比较晦涩难懂,这里讲一下公式是如何实现计算距离的。

这里不贴整篇的数学公式了,可以在官网中找到,官网提供的公式有数学公式和javascript使用的公式。感兴趣的同学也可以翻出Vincenty的论文看看。

javascript 公式

js 复制代码
const L = λ2 - λ1; // L = difference in longitude, U = reduced latitude, defined by tan U = (1-f)·tanφ.
const tanU1 = (1-f) * Math.tan(φ1), cosU1 = 1 / Math.sqrt((1 + tanU1*tanU1)), sinU1 = tanU1 * cosU1;
const tanU2 = (1-f) * Math.tan(φ2), cosU2 = 1 / Math.sqrt((1 + tanU2*tanU2)), sinU2 = tanU2 * cosU2;

let λ = L, sinλ = null, cosλ = null;    // λ = difference in longitude on an auxiliary sphere
let σ = null, sinσ = null, cosσ = null; // σ = angular distance P₁ P₂ on the sphere
let cos2σₘ = null;                      // σₘ = angular distance on the sphere from the equator to the midpoint of the line
let cosSqα = null;                      // α = azimuth of the geodesic at the equator

let λʹ = null;
do {
    sinλ = Math.sin(λ);
    cosλ = Math.cos(λ);
    const sinSqσ = (cosU2*sinλ) * (cosU2*sinλ) + (cosU1*sinU2-sinU1*cosU2*cosλ)**2);
    sinσ = Math.sqrt(sinSqσ);
    cosσ = sinU1*sinU2 + cosU1*cosU2*cosλ;
    σ = Math.atan2(sinσ, cosσ);
    const sinα = cosU1 * cosU2 * sinλ / sinσ;
    cosSqα = 1 - sinα*sinα;
    cos2σₘ = cosσ - 2*sinU1*sinU2/cosSqα;
    const C = f/16*cosSqα*(4+f*(4-3*cosSqα));
    λʹ = λ;
    λ = L + (1-C) * f * sinα * (σ + C*sinσ*(cos2σₘ+C*cosσ*(-1+2*cos2σₘ*cos2σₘ)));
} while (Math.abs(λ-λʹ) > 1e-12);

const uSq = cosSqα * (a*a - b*b) / (b*b);
const A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
const B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
const Δσ = B*sinσ*(cos2σₘ+B/4*(cosσ*(-1+2*cos2σₘ*cos2σₘ)-B/6*cos2σₘ*(-3+4*sinσ*sinσ)*(-3+4*cos2σₘ*cos2σₘ)));

const s = b*A*(σ-Δσ); // s = length of the geodesic

const α1 = Math.atan2(cosU2*sinλ,  cosU1*sinU2-sinU1*cosU2*cosλ); // initial bearing
const α2 = Math.atan2(cosU1*sinλ, -sinU1*cosU2+cosU1*sinU2*cosλ); // final bearing

公式讲解

要看懂这些内容,首先要知道一些数字(系数)代表的是什么。这些数字并不是随意写的,都是通过前文提到的WGS84椭球模型提供的系数,这些系数是通过数学推导和地球物理模型得出的,用来确保精度和维持稳定性的。

4096-768320-175 是多项式中的系数,这些数与与uSq(u的平方)相乘,用于计算A的近似值

256-12874-47 也是多项式中的系数,它们与uSq相乘,用于计算B的近似值。

这些系数是通过地球椭球模型计算得出的,在公式中用于提高精度。

ts 复制代码
// 计算u的平方 
const uSq = cosSqAlpha * (a * a - b * b) / (b * b);

const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
const B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));

代码中有一个do while 循环,用来迭代计算公式的,那么我们就可以加一个最大迭代次数来限制一下这个迭代最大执行几次let iterLimit = 100;并在迭代循环中加入这个控制变量,如果迭代次数超过最大次数,返回的将是不可靠的值。

ts 复制代码
iterLimit--;

if (iterLimit === 0) {
    return NaN; // 超过迭代次数上限,返回不可靠的结果
}

在循环中有一个计算C的值,这是Vincenty公式中的中间值,用于更新经度差 λ 的值 对应原数学公式中的:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ∗ C ∗ = ∗ f ∗ / 16 ⋅ c o s 2 α ⋅ [ 4 + f ⋅ ( 4 − 3 ⋅ c o s 2 α ) ] *C* = *f*/16 · cos² α · [4 + f · (4 − 3 · cos² α)] </math>∗C∗ = ∗f∗/16⋅cos2α⋅[4+f⋅(4−3⋅cos2α)]

16 是分母中的一个常数,用于缩放扁率 f 的影响。34 是与扁率和方位角余弦值的平方相乘的系数。

在通过一系列迭代后,最后再计算出实际的距离,distance距离后面/1000是我为了和前文Haversine输出相同单位而设定的

ts 复制代码
// 计算δσ  
const deltaSigma = B * sinSigma * (
    cos2SigmaM + B / 4 * (
        cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM))
);

const distance = b * A * (sigma - deltaSigma) / 1000;

对应数学公式
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> Δ σ = ∗ B ∗ ⋅ s i n σ ⋅ c o s 2 σ m + ∗ B ∗ / 4 ⋅ [ c o s σ ⋅ ( − 1 + 2 ⋅ c o s 2 2 σ m ) − ∗ B ∗ / 6 ⋅ c o s 2 σ m ⋅ ( − 3 + 4 ⋅ s i n 2 σ ) ⋅ ( − 3 + 4 ⋅ c o s 2 2 σ m ) ] Δσ = *B* · sin σ · {cos 2σm + *B*/4 · [cos σ · (−1 + 2 · cos² 2σm) − *B* / 6 · cos 2σm · (−3 + 4 · sin² σ) · (−3 + 4 · cos² 2σm)]} </math>Δσ= ∗B∗ ⋅sinσ⋅cos2σm + ∗B∗/4⋅[cosσ⋅(−1+2⋅cos22σm) − ∗B∗ /6⋅cos2σm ⋅(−3+4⋅sin2σ)⋅(−3+4⋅cos22σm)]


<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> s = ∗ b ∗ ⋅ ∗ A ∗ ⋅ ( σ − Δ σ ) s = *b*·*A*·(σ−Δσ) </math>s = ∗b∗⋅∗A∗⋅(σ−Δσ)

三种方法距离对比

上图是百度自带测绘工具输出的色值(右下角),再上面的两个数值是咱们通过上面介绍的公式计算出来的,从理论上看,vincenty公式使用的系数是WGS84,而百度用的是另一种坐标系统(BD09),应该是有偏差的(理论上),但是并未验证,不过如果有偏差,百度也提供了相应的坐标转换工具。

具体参考 开发指南

结语

Haversine公式是一种简化的计算方法,它将地球视为一个完美的球体,通过计算大圆上的劣弧长度来估算两点间的距离。尽管这种方法简单易行,但由于忽略了地球实际形状的复杂性(如扁率和椭球体的特性),其计算精度相对较低,但是公式不复杂,在进行短距离的计算,可以选用。

而Vincenty公式则采用了更为复杂和精确的方法。它将地球视为一个椭球体,并通过建立地球的数学模型模拟。通过地球模型提供的扁率、长半轴、短半轴等关键参数,并利用这些参数以及一系列复杂的数学公式来进行精确的推导演算。因此,Vincenty公式能够更准确地计算地球上两点之间的大圆距离。

相关推荐
Мартин.3 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。4 小时前
案例-表白墙简单实现
前端·javascript·css
数云界4 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd4 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常4 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer4 小时前
Vite:为什么选 Vite
前端
小御姐@stella4 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing4 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd4 小时前
前端知识汇总(持续更新)
前端
万叶学编程7 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js