Skip to content

Commit 36b3a1a

Browse files
committed
feat: add initial bearing calculation function between geographic points
1 parent e8002eb commit 36b3a1a

File tree

1 file changed

+78
-0
lines changed

1 file changed

+78
-0
lines changed

maths/bearing.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""
2+
Initial bearing (forward azimuth) between two geographic points.
3+
4+
Points are (latitude, longitude) in decimal degrees. Returns bearing in degrees
5+
clockwise from true north in the range [0, 360).
6+
7+
Reference: https://en.wikipedia.org/wiki/Bearing_(navigation)
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import math
13+
14+
15+
def initial_bearing(
16+
origin: tuple[float, float], destination: tuple[float, float]
17+
) -> float:
18+
"""
19+
Compute the initial bearing from `origin` to `destination`.
20+
21+
Parameters
22+
----------
23+
origin, destination : tuple[float, float]
24+
(latitude, longitude) in decimal degrees.
25+
26+
Returns
27+
-------
28+
float
29+
Initial bearing in degrees, clockwise from north in [0, 360).
30+
31+
Raises
32+
------
33+
TypeError
34+
If inputs are not 2-tuples of numbers.
35+
ValueError
36+
If the two points are identical (bearing undefined).
37+
38+
Examples
39+
>>> round(initial_bearing((50.066389, -5.714722), (58.643889, -3.07)), 3)
40+
9.12
41+
>>> round(initial_bearing((0.0, 0.0), (1.0, 1.0)), 3)
42+
44.996
43+
>>> initial_bearing((0.0, 0.0), (0.0, 0.0))
44+
Traceback (most recent call last):
45+
...
46+
ValueError: origin and destination are the same point; bearing is undefined
47+
"""
48+
try:
49+
lat1, lon1 = float(origin[0]), float(origin[1])
50+
lat2, lon2 = float(destination[0]), float(destination[1])
51+
except Exception as exc:
52+
raise TypeError(
53+
"origin and destination must be 2-tuples of numeric values"
54+
) from exc
55+
56+
if lat1 == lat2 and lon1 == lon2:
57+
raise ValueError(
58+
"origin and destination are the same point; bearing is undefined"
59+
)
60+
61+
# convert degrees to radians
62+
phi1 = math.radians(lat1)
63+
phi2 = math.radians(lat2)
64+
delta_lambda = math.radians(lon2 - lon1)
65+
66+
x = math.sin(delta_lambda) * math.cos(phi2)
67+
y = math.cos(phi1) * math.sin(phi2) - math.sin(phi1) * math.cos(phi2) * math.cos(
68+
delta_lambda
69+
)
70+
71+
theta = math.atan2(x, y) # result in radians relative to north
72+
bearing = (math.degrees(theta) + 360.0) % 360.0
73+
return bearing
74+
75+
76+
if __name__ == "__main__":
77+
# simple demonstration
78+
print(initial_bearing((50.066389, -5.714722), (58.643889, -3.07)))

0 commit comments

Comments
 (0)