緯度経度からの距離や方角の導出
緯度(latitude)と経度(longitude)が判っている2点の距離の導出。
- 半径1球体の中心
をO 、北極の座標を(0,0,0) とし、北緯0東経0の点を(0, 0, 1) 、北緯0東経90の点を(1, 0, 0) とする座標系を用いる(西経や南緯はマイナス角度)(0, 1, 0) - このとき、緯度
のz座標はP 、緯度\sin P での(赤道面と平行な)円の半径はP になる\cos P - よって緯度
経度P の点の座標はQ である(\cos P\cos Q, \cos P\sin Q, \sin P) - 2点の距離は、2点と中心
を含む(半径1の)大円上での2点のラジアン角度O そのものであるt - 半径1なので大円上の2点の中心からの長さは1であり、その内積は
である\cos t - 緯度
経度P_1 と緯度Q_1 経度P_2 の二点の内積は、3の座標値より、Q_2
\cos P_1\cos P_2\cos Q_1\cos Q_2 + \cos P_1\cos P_2\sin Q_1\sin Q_2 + \sin P_1\sin P_2
である - よって
\cos t = \cos P_1\cos P_2\cos Q_1\cos Q_2 + \cos P_1\cos P_2\sin Q_1\sin Q_2 + \sin P_1\sin P_2 - よって
t = \arccos(\cos P_1\cos P_2\cos Q_1\cos Q_2 + \cos P_1\cos P_2\sin Q_1\sin Q_2 + \sin P_1\sin P_2) - 地球上での距離は地球の平均半径6371000mを
にかけることで求まるt
この手法は回転楕円体である地球では厳密ではない。しかし、少ない計算で求まる利点がある。
(さらに三角関数の加法定理で、
const distance = ([lat1, lon1], [lat2, lon2]) => {
const {PI, acos, sin, cos} = Math, rad = PI / 180, r = 6371000;
const [p1, p2, q1, q2] = [lat1, lat2, lon1, lon2].map(v => v * rad);
return r * acos(cos(p1) * cos(p2) * cos(q1 - q2) + sin(p1) * sin(p2));
};
地球の平均半径の導出。
- 平均半径は、楕円体を同じ体積の真の球体とみなしたときの半径のこと。
- 球の体積は
\frac{4\pi r^3}{3} - 楕円体はx軸y軸z軸ごとに別々の経をもち、それぞれa,b,cとすると、体積は
\frac{4\pi abc}{3} - この2つを等しいとみなすことによって、平均半径
r = \sqrt[3]{abc} - 地球は球の上下を少し潰したもの(回転楕円体)とみなし、a=b=6378137m、c=6356752mである
- 地球の平均半径は6371000.685...m
const a = 6378137, c = 6356752;
const r = Math.cbrt(a * a * c);
haversine法
-
と定義する\text{hav}(x) = \frac{1 - \cos x}{2} - 緯度経度表示の二点の内積より
-
より、\cos x =1-2\text{hav}(x) 1-2\text{hav}(t)=\cos P_1\cos P_2(1-2\text{hav}(Q_1-Q_2))+\sin P_1\sin P_2 - 更に変形していくと、
- よって
\text{hav}(t)=\text{hav}(P_1-P_2)+\cos P_1\cos P_2\text{hav}(Q_1-Q_2)
\text{hav}^{-1}(y) = \arccos(1 - 2y)
const distance = ([lat1, lon1], [lat2, lon2]) => {
const {PI, acos, cos} = Math, rad = PI / 180, r = 6371000;
const hav = x => (1 - cos(x)) / 2, ihav = y => acos(1 - 2 * y);
const [p1, p2, q1, q2] = [lat1, lat2, lon1, lon2].map(v => v * rad);
return r * ihav(hav(p1 - p2) + cos(p1) * cos (p2) * hav(q1 - q2));
};
haversine法では、cos/acosだけで計算できる(cos表があれば手計算できる)。
geodesic inverse/direct
上記リンクの途中のcは球面での内積そのもの。「ゾーンの判断」以降補正していくよう。
- https://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf
- https://ja.wikipedia.org/wiki/Vincenty法
- https://en.wikipedia.org/wiki/Vincenty's_formulae
逆解法(inverse)が二点から距離とそれぞれの方位角を求める方法で、順解法(direct)が1点と方位角と距離で行き先を求める方法。
緯度経度同士での方角
- 緯度
経度P_1 から緯度Q_1 経度P_2 への向きとは、Q_2 から中心(P_1, Q_1) への射影面上にあるO からO への向きである(P_2, Q_2) - 経度差
とすると、dQ=Q_2-Q_1 の値を0に調整したy のxyz座標は(P_1, Q_1) 、(\cos P_1, 0, \sin P_1) のxyz座標は(P_2, Q_2) (\cos P_2\cos dQ, \cos P_2\sin dQ, \sin P_2) -
を座標(P_1, Q_1) に移す回転はY軸周りで(1, 0, 0) 度回転させることである-P_1 - Y軸
度回転の行列は-P_1
-
にY軸(P_2, Q_2) 度回転をかけた結果は、-P_1
- X軸の先から中心へ眺めるYZ平面上での
の位置は、(P_2, Q_2) (Y, Z) = (\cos P_2 \sin dQ, -\sin P_1\cos P_2\cos dQ + \cos P_1\sin P_2) - 北を0度、東を90度とする場合の方角
とすると、a \tan a = \frac{\cos P_2\sin dQ}{-\sin P_1\cos P_2\cos dQ + \cos P_1\sin P_2} - よって、
a = \text{atan2}(\cos P_2\sin dQ, -\sin P_1\cos P_2\cos dQ + \cos P_1\sin P_2) - atan2の戻り値は
を返すので、(-\pi, \pi] 度に変えるには[0, 360) を足してから180を掛け\pi で割る\pi
const angle = ([lat1, lon1], [lat2, lon2]) => {
const {PI, sin, cos, atan2} = Math, rad = PI / 180;
const [p1, p2, q1, q2] = [lat1, lat2, lon1, lon2].map(v => v * rad);
const dq = q2 - q1, cosP2 = cos(p2);
const y = cosP2 * sin(dq);
const z = cos(p1) * sin(p2) - sin(p1) * cosP2 * cos(dq);
return atan2(y, z);
};
ちなみに、この方位はメルカトル図法(円筒モデル)上での向きではない。2点の最短距離の線(大円)の向きである。
起点の緯度経度
- 半径1の単位球では、距離
の移動とは、角度d ラジアン回転させた先に移ることである(一周回ればd 移動する)2\pi - 球の半径が
のときの距離R の移動とは、角度d ラジアン回転させた先である(\frac{d}{R} 移動させれば一周して戻ってくる)d=2\pi R - 単位球の北極を
、起点を(0, 0, 1) 、起点の経度+90度を(1, 0, 0) とするとき、(0, 1, 0) から角度(1, 0, 0) (真北が0度で時計回りに増加)の向きに距離a の移動とは、Y軸を反時計回りにd ラジアン回転し、そのあとでX軸を時計回りにd ラジアン回転させることに等しいa - そこからさらにY軸反時計回りに緯度ぶんの
(のラジアン値の)回転させれば、目的のxyz座標に移動するP_1
- (以降、計算式では
、P_1 等はラジアン値に変換したものとして扱う)Q_1
- この座標値と、方角を求めたときの
のの座標値と比較することで、緯度(P_2, dQ) と経度の差P_2 が求まるdQ - Y軸反時計回り
ラジアンの回転行列s Y_s
- X軸時計回り
ラジアンの回転行列t X_t
-
とすると、移動先はI=(1, 0, 0) Y_{P_1} X_a Y_d I - 行列計算すると
- この値と、方角のときの
と比較させる(P_2, Q_2)=(\cos P_2\cos dQ, \cos P_2\sin dQ , \sin P_2) \cos P_2\cos dQ = \cos P_1\cos d - \sin P_1\sin d\cos a \cos P_2\sin dQ = \sin d\sin a \sin P_2 = \sin P_1\cos d + \cos P_1\sin d\cos a
- まず緯度
が求まるP_2 = \arcsin(\sin P_1 \cos d + \cos P_1\sin d \cos a) - つぎに、
と\sin dQ ともに\cos dQ がかかっており、\cos P_2 が0のときに計算できなくなるので、\cos P_2 として、atan2を用いて\tan dQ = \frac{\sin dQ}{\cos dQ} を算出するdQ
dQ = \text{atan2}(\sin d\sin a, \cos P_1\cos d - \sin P_1\sin d\cos a)
- 経度
Q_2 = Q_1 + dQ - どちらもラジアンなので360度に変換する
\text{緯度} = \frac{P_2 \times 180}{\pi} \text{経度} = (\frac{Q_2 \times 180}{\pi} + 360) % 360 - 180
// angleは真北0時計回りのラジアン
// distanceはメートル
const destination = ([lat1, lon1], angle, distance) => {
const {PI, sin, cos, asin, atan2} = Math;
const r = 6371000, rad = PI / 180;
const d = distance / r, p1 = lat * rad, q1 = lng * rad;
const p2 = asin(sin(p1) * cos(d) + cos(p1) * sin(d) * cos(angle));
const q2 = q1 + atan2(sin(angle) * sin(d), cos(p1) * cos(d) - sin(p1) * sin(d) * cos(angle));
return [p2 / rad, (q2 / rad + 360) % 360 - 180];
};
atan2(y, x)の引数2つにともに
- 上辺y:
\cos P_1 \sin d\sin a - 下辺x:
\cos P_1\cos P_1\cos d - \cos P_1\sin P_1\sin d\cos a = \cos d -\sin P_1\sin P_1\cos d - \sin P_1\cos P_1\sin d\cos a - 先のZ値の対比
を用いることで、下辺x:\sin P_2 = \sin P_1\cos d + \cos P_1\sin d\cos a \cos d -\sin P_1\sin P_2
よって以下の様に変形し、演算回数を(掛け算ひとつ)減らすことができる
const destination = ([lat1, lon1], angle, distance) => {
const {PI, sin, cos, asin, atan2} = Math, r = 6371000, rad = PI / 180;
const d = distance / r, p1 = lat * rad, q1 = lng * rad;
const cosP1 = cos(p1), sinP1 = sin(p1), cosD = cos(d), sinD = sin(d);
const sinP2 = sinP1 * cosD + cosP1 * sinD * cos(angle);
const p2 = asin(sinP2);
const q2 = q1 + atan2(cosP1 * sinD * sin(angle), cosD - sinP1 * sinP2);
return [p2 / rad, (q2 / rad + 360) % 360 - 180];
};
参考: 前述の画像生成用のスクリプト(python3, numpy, matplotlib)
- Demo on Google Colab: https://colab.research.google.com/drive/1zckfZKqwxYHy2kG4JVXi0srn1fff4Mm2?usp=sharing
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.gca(projection="3d")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_xticks([-1, 0, 1])
ax.set_yticks([-1, 0, 1])
ax.set_zticks([-1, 0, 1])
def sphere():
u = np.linspace(0, 2 * np.pi, 100)
v = np.linspace(0, np.pi, 100)
x = np.outer(np.cos(u), np.sin(v))
y = np.outer(np.sin(u), np.sin(v))
z = np.outer(np.ones(np.size(u)), np.cos(v))
ax.plot_surface(x, y, z, rstride=4, cstride=4, color="b", linewidth=0, alpha=0.2)
pass
sphere()
p = np.pi / 4
q = np.pi / 3
ax.plot([0, 1], [0, 0], [0, 0], color="black")
ax.plot([0, 0], [0, 0], [0, 1], color="black")
# lat
ax.plot([0, np.cos(p)], [0, 0], [0, np.sin(p)], color="b")
ax.plot(np.cos(np.linspace(0, p, 10)), np.zeros(10), np.sin(np.linspace(0, p, 10)), color="b")
ax.plot(np.cos(np.linspace(p, np.pi/2, 10)), np.zeros(10), np.sin(np.linspace(p, np.pi/2, 10)), linestyle="dotted", color="b")
# lng
ax.plot([0, np.cos(p)], [0, 0], [np.sin(p), np.sin(p)], color="black")
ax.plot([0, np.cos(p) * np.cos(q)], [0, np.cos(p) * np.sin(q)], [np.sin(p), np.sin(p)], color="r")
ax.plot(np.cos(p) * np.cos(np.linspace(0, q, 10)), np.cos(p) * np.sin(np.linspace(0, q, 10)), np.sin(p) * np.ones(10), color="r")
ax.plot([0, np.cos(q)], [0, np.sin(q)], [0, 0], linestyle="dotted", color="m")
ax.plot(np.cos(np.linspace(0, q, 10)), np.sin(np.linspace(0, q, 10)), np.zeros(10), color="m")
ax.plot(np.cos(np.linspace(0, np.pi/2, 10)) * np.cos(q), np.cos(np.linspace(0, np.pi / 2, 10)) * np.sin(q), np.sin(np.linspace(0, np.pi/2, 10)), linestyle="dotted", color="m")
#ax.text(0, 0, 0, "O")
ax.text(0, 0, 1, "N")
ax.text(0, 0, -1, "S")
ax.text(0, 1, 0, "E")
ax.text(0, -1, 0, "W")
ax.text(1, 0, -0.1, "(0, 0)")
ax.text(np.cos(p), 0, np.sin(p), "(lat, 0)")
ax.text(np.cos(q), np.sin(q), 0, "(0, lon)")
ax.text(np.cos(p) * np.cos(q), np.cos(p) * np.sin(q), np.sin(p), "(lat, lon)")
ax.view_init(elev=18, azim=0)
plt.show()
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.gca(projection="3d")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_xticks([-1, 0, 1])
ax.set_yticks([-1, 0, 1])
ax.set_zticks([-1, 0, 1])
def sphere():
u = np.linspace(0, 2 * np.pi, 100)
v = np.linspace(0, np.pi, 100)
x = np.outer(np.cos(u), np.sin(v))
y = np.outer(np.sin(u), np.sin(v))
z = np.outer(np.ones(np.size(u)), np.cos(v))
ax.plot_surface(x, y, z, rstride=4, cstride=4, color="b", linewidth=0, alpha=0.2)
pass
sphere()
d = np.pi / 4
a = np.pi / 3
ax.plot([0, 1], [0, 0], [0, 0], color="black")
ax.plot([0, 0], [0, 0], [0, 1], color="black")
# distance
ax.plot([0, np.cos(d)], [0, 0], [0, np.sin(d)], color="b")
ax.plot(np.cos(np.linspace(0, d, 10)), np.zeros(10), np.sin(np.linspace(0, d, 10)), color="b")
ax.plot(np.cos(np.linspace(d, np.pi/2, 10)), np.zeros(10), np.sin(np.linspace(d, np.pi/2, 10)), linestyle="dotted", color="b")
# angle
ax.plot([0, 0], [0, np.sin(a)], [0, np.cos(a)], color="r")
ax.plot(np.zeros(10), np.sin(np.linspace(0, a, 10)), np.cos(np.linspace(0, a, 10)), color="r")
# mix
ax.plot([0, np.cos(d)], [0, np.sin(a) * np.sin(d)],[0, np.cos(a) * np.sin(d)], color="g")
ax.plot(np.cos(np.linspace(0, d, 10)), np.sin(a) * np.sin(np.linspace(0, d, 10)), np.cos(a) * np.sin((np.linspace(0, d, 10))), color="g")
ax.plot(np.cos(np.linspace(d, np.pi/2,10)), np.sin(a) * np.sin(np.linspace(d, np.pi/2, 10)), np.cos(a) * np.sin((np.linspace(d, np.pi/2, 10))), linestyle="dotted", color="g")
ax.plot([np.cos(d)] * 10, np.sin(np.linspace(0, a, 10)) * np.sin(d), np.cos(np.linspace(0, a, 10)) * np.sin(d), linestyle="dotted", color="r")
#ax.text(0, 0, 0, "O")
ax.text(0, 0, 1, "N")
ax.text(0, 0, -1, "S")
ax.text(0, 1, 0, "E")
ax.text(0, -1, 0, "W")
ax.text(1, 0, -0.1, "P1")
ax.text(np.cos(d), 0, np.sin(d), "d")
ax.text(0, np.sin(a/2), np.cos(a/2), "a")
ax.text(np.cos(d), np.sin(a) * np.sin(d), np.cos(a) * np.sin(d), "P2")
ax.view_init(elev=18, azim=0)
plt.show()
付録1: 内積と角度
- 二次元の点
と点(x_1, y_1) の内積は(x_2, y_2) 。x_1 x_2+y_1 y_2 - 点を中心
からの角度(0, 0) と長さa とで表現すると、r 。(r \cos a, r \sin a) -
の加法定理の一つ:\cos \cos a \cos b +\sin a\sin b = \cos(a-b) = \cos(b-a) - 二点を
、(r_1 \cos a_1, r_1 \sin a_1) としたとき、それらの内積は、(r_2 \cos a_2, r_2 \sin a_2) r_1 r_2 \cos a_1 \cos a_2 +r_1 r_2\sin a_1\sin a_2 = r_1 r_2\cos(a_1-a_2) -
は二点の間の角度。二点を同時に回転させ、座標を変えても二点の間の角度の値は変わらないa_1-a_2
3次元の場合
- 一つ目の点を
、2つ目をP_1=(r_1, 0, 0) とXZ平面上で中心P_1 で角度O をなす点a とすると、二点と中心で作る面の法線はP_2=(r_2 \cos a, 0, r_2 \sin a) となる(0, 1, 0) - 2点を目的の位置に調整することは、法線の座標を球面上の適当な位置へ移動させることである。これは、はじめZ軸つぎにY軸の適当な角度での二回転を適用することで可能。
- Z軸回転
- Y軸回転
- よって3次元でも、(2点がどんな位置にあろうがその)内積は二点の角度aに基づく値となる
付録2: 弧度法
- 角度を0-2πとする弧度法とは、角度を半径の長さを1とする単位円上の弧の長さで表現したもの
Vincenty法
Wikipediaにある実装をそのまま適用
export const wgs84 = Object.freeze({
a: 6378137.06,
b: 6356752.314245,
f: 1 / 298.257223563,
});
export const grs80 = Object.freeze({
a: 6378137,
b: 6356582.314,
f: 1 / 298.257222101,
});
// Vincenty formula: https://en.wikipedia.org/wiki/Vincenty%27s_formulae
export const inverse = ([lat1, lon1], [lat2, lon2], {a, b, f} = wgs84, eps = Number.EPSILON) => {
const {abs, atan, atan2, cos, sin, tan, PI, sqrt} = Math;
const P1 = lat1 / 180 * PI, P2 = lat2 / 180 * PI, L = (lon2 - lon1) / 180 * PI;
const U1 = atan((1 - f) * tan(P1)), U2 = atan((1 - f) * tan(P2));
const sinU1 = sin(U1), cosU1 = cos(U1), sinU2 = sin(U2), cosU2 = cos(U2);
const cosU1sinU2 = cosU1 * sinU2, sinU1cosU2 = sinU1 * cosU2, sinU1sinU2 = sinU1 * sinU2, cosU1cosU2 = cosU1 * cosU2;
for (let l = L, i = 0; i < 1000; i++) {
const sinL = sin(l), cosL = cos(l);
const sinD = sqrt((cosU2 * sinL) ** 2 + (cosU1sinU2 - sinU1cosU2 * cosL) ** 2);
const cosD = sinU1sinU2 + cosU1cosU2 * cosL;
const D = atan2(sinD, cosD);
const sinA = cosU1cosU2 * sinL / sinD;
const cos2A = 1 - sinA ** 2;
const cos2Dm = cosD - 2 * sinU1sinU2 / cos2A;
const C = f / 16 * cos2A * (4 + f * (4 - 3 * cos2A));
const cos22Dm = cos2Dm ** 2;
const l_ = l;
l = L + (1 - C) * f * sinA * (D + C * sinD * (cos2Dm + C * cosD * (-1 + 2 * cos22Dm)));
if (abs(l - l_) < eps) {
//console.log(i);
const a2 = a ** 2, b2 = b ** 2, sin2D = sinD ** 2;
const u2 = cos2A * (a2 - b2) / b2;
const A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)));
const B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)));
const dD = B * sinD * (cos2Dm + B / 4 * (cosD * (-1 + 2 * cos22Dm) - B / 6 * cos2Dm * (-3 + 4 * sin2D) * (-3 + 4 * cos22Dm)));
const s = b * A * (D - dD);
const A1 = atan2(cosU2 * sinL, cosU1sinU2 - sinU1cosU2 * cosL); // dir at latlon1
const A2 = atan2(cosU1 * sinL, -sinU1cosU2 + cosU1sinU2 * cosL); // dir at latlong2, but dir from latlon1 to latlon2
return {s, A1, A2, a1to2: (A1 / PI * 180 + 360) % 360, a2to1: (A2 / PI * 180 + 180) % 360};
}
}
throw Error();
};
export const direct = ([lat1, lon1], a1, s, {a, b, f} = wgs84, eps = Number.EPSILON) => {
const {abs, atan, atan2, cos, sin, tan, PI, sqrt} = Math;
const P1 = lat1 / 180 * PI, L1 = lon1 / 180 * PI, A1 = a1 / 180 * PI;
const tanU1 = (1 - f) * tan(P1);
const U1 = atan(tanU1);
const cosU1 = cos(U1), sinU1 = sin(U1), cosA1 = cos(A1), sinA1 = sin(A1);
const D1 = atan2(tanU1, cosA1);
const sinA = cosU1 * sinA1;
const cos2A = 1 - sinA ** 2;
const u2 = cos2A * (a ** 2 - b ** 2) / (b ** 2);
const A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)));
const B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)));
const D0 = s / b / A;
for (let D = D0, i = 0; i < 1000; i++) {
const sinD = sin(D), cosD = cos(D);
const cos2Dm = cos(2 * D1 + D);
const cos22Dm = cos2Dm ** 2, sin2D = sinD ** 2;
const dD = B * sinD * (cos2Dm + B / 4 * (cosD * (-1 + 2 * cos22Dm) - B / 6 * cos2Dm * (-3 + 4 * sin2D) * (-3 + 4 * cos22Dm)));
const D_ = D;
D = D0 + dD;
if (abs(D - D_) < eps) {
//console.log(i);
const P2 = atan2(sinU1 * cosD + cosU1 * sinD * cosA1, (1 - f) * sqrt(sinA ** 2 + (sinU1 * sinD - cosU1 * cosD * cosA1) ** 2));
const l = atan2(sinD * sinA1, cosU1 * cosD - sinU1 * sinD * cosA1);
const C = f / 16 * cos2A * (4 + f * (4 - 3 * cos2A));
const L = l - (1 - C) * f * sinA * (D + C * sinD * (cos2Dm + C * cosD * (-1 + 2 * cos22Dm)));
const L2 = L + L1;
const A2 = atan2(sinA, -sinU1 * sinD + cosU1 * cosD * cosA1); // dir at latlong2, but dir from latlon1 to latlon2
return {latlon2: [P2 / PI * 180, L2 / PI * 180], A2, a2to1: (A2 / PI * 180 + 180) % 360};
}
}
throw Error();
};
方位角が2つあるのは、回転楕円体のため、同じ直線でも、緯度経度1を中心に置いて見る角度と、緯度経度2を中心に置いて見る角度は違うから。二点の間を結ぶ直線が2つあるわけではない。
また、atan2で出す角度A1とA2はどちらも、真北を0真東をπ/2とした、緯度経度1から緯度経度2へ向かう直線の方角である。緯度経度2から緯度経度1への方角にするには反転させるために180度足す。