Skip to content

Bearing & Destination

This guide covers two complementary operations on the WGS-84 ellipsoid:

  • Bearing (inverse problem): Given two points, what is the initial direction from one to the other?
  • Destination (direct problem): Given a starting point, a direction, and a distance, where do you end up?

Together, these complete Vincenty's inverse/direct pair and enable powerful geospatial workflows.

Understanding Bearing

A bearing (or forward azimuth) is the initial compass direction you would travel from point A to reach point B along the shortest path on the Earth's surface (the geodesic). It is measured in degrees clockwise from true north:

Bearing Direction
North
90° East
180° South
270° West

Bearing changes along a geodesic

On an ellipsoid, the bearing is not constant along a geodesic (unlike a rhumb line). The bearing at point A towards B differs from the bearing at B towards A by roughly 180° — but not exactly, due to the Earth's curvature and flattening. This is why Vincenty's inverse formula computes both a forward azimuth and a back azimuth.

Basic Usage

from geodistpy import bearing

# Berlin → Paris
b = bearing((52.5200, 13.4050), (48.8566, 2.3522))
print(f"Bearing: {b:.2f}°")  # ~245.58° (roughly southwest)

# Cardinal directions on the equator
print(bearing((0, 0), (0, 1)))    # 90.0  (due east)
print(bearing((0, 0), (1, 0)))    # 0.0   (due north)
print(bearing((1, 0), (0, 0)))    # 180.0 (due south)
print(bearing((0, 1), (0, 0)))    # 270.0 (due west)

Bearing is Not Symmetric

The bearing from A → B is different from B → A:

from geodistpy import bearing

berlin = (52.5200, 13.4050)
paris  = (48.8566, 2.3522)

b_ab = bearing(berlin, paris)
b_ba = bearing(paris, berlin)

print(f"Berlin → Paris: {b_ab:.2f}°")   # ~245.58°
print(f"Paris → Berlin: {b_ba:.2f}°")   # ~58.29°
print(f"Difference: {abs(b_ab - b_ba):.2f}°")  # ~187.29° (not exactly 180°)

This difference from 180° arises because the geodesic curves on the ellipsoid.

Use Cases

  • Navigation: Determine which direction to travel
  • Compass headings: Convert geodesic paths to bearing instructions
  • Sector analysis: Classify the direction of movement (N/S/E/W quadrant)
  • Antenna alignment: Point directional antennas towards a target

Understanding Destination

The destination function solves Vincenty's direct problem: given a starting point, an initial bearing, and a distance, it computes the endpoint.

Basic Usage

from geodistpy import destination

# Travel 500 km due east from Berlin
lat, lon = destination((52.5200, 13.4050), 90.0, 500, metric='km')
print(f"Destination: ({lat:.4f}, {lon:.4f})")

# Travel 100 miles due north from the equator
lat, lon = destination((0.0, 0.0), 0.0, 100, metric='mile')
print(f"Destination: ({lat:.4f}, {lon:.4f})")

Supported Distance Units

The metric parameter controls the unit of the distance argument:

from geodistpy import destination

start = (52.5200, 13.4050)

# All produce the same destination:
destination(start, 90.0, 500_000, metric='meter')
destination(start, 90.0, 500, metric='km')
destination(start, 90.0, 500 / 1.609344, metric='mile')
destination(start, 90.0, 500 / 1.852, metric='nmi')

Longitude Normalisation

Destination longitudes are automatically normalised to the range [-180, 180]:

from geodistpy import destination

# Travel east past the antimeridian
lat, lon = destination((0.0, 170.0), 90.0, 2000, metric='km')
print(f"lon = {lon:.2f}")  # Normalised to [-180, 180]

Roundtrip: Bearing → Destination

The bearing and destination functions are inverses of each other. You can verify this with a roundtrip:

from geodistpy import bearing, destination, geodist

# Given two points
A = (52.5200, 13.4050)  # Berlin
B = (48.8566, 2.3522)   # Paris

# Step 1: Compute bearing and distance
b = bearing(A, B)
d = geodist(A, B, metric='km')
print(f"Bearing: {b:.4f}°, Distance: {d:.2f} km")

# Step 2: Reconstruct B using destination
B_reconstructed = destination(A, b, d, metric='km')
print(f"Original:      ({B[0]:.4f}, {B[1]:.4f})")
print(f"Reconstructed: ({B_reconstructed[0]:.4f}, {B_reconstructed[1]:.4f})")
# The two should match to ~4 decimal places

Real-World Example: Search Sector

Create a search area by projecting points at various bearings from a centre:

from geodistpy import destination

# Centre of search area
centre = (48.8566, 2.3522)  # Paris
radius_km = 50

# Generate 8 compass points around the centre
compass = {
    'N': 0, 'NE': 45, 'E': 90, 'SE': 135,
    'S': 180, 'SW': 225, 'W': 270, 'NW': 315,
}

for name, bearing_deg in compass.items():
    lat, lon = destination(centre, bearing_deg, radius_km, metric='km')
    print(f"  {name:>2}: ({lat:.4f}, {lon:.4f})")

How It Works: Vincenty's Formulas

Inverse (Bearing)

Vincenty's inverse formula iteratively solves for the geodesic distance and azimuths between two points on an ellipsoid. The key outputs are:

  • s — geodesic distance
  • α₁ — forward azimuth (bearing at start point)
  • α₂ — back azimuth (bearing at end point)

Geodistpy's geodesic_vincenty_inverse_full() returns all three in a single Numba-JIT pass. The high-level bearing() function extracts just the forward azimuth.

Direct (Destination)

Vincenty's direct formula takes a starting point, forward azimuth α₁, and distance s, then iteratively computes:

  • φ₂, λ₂ — latitude and longitude of the destination point
  • α₂ — back azimuth at the destination

Geodistpy's geodesic_vincenty_direct() implements this as a Numba-JIT function for maximum performance.

Convergence and Fallback

Both formulas iterate until convergence (threshold: 10⁻¹¹). In the rare case (~0.01%) that the inverse formula fails to converge (typically near-antipodal points), geodistpy falls back to the slower but always-convergent GeographicLib algorithm.

Performance

Both bearing() and destination() benefit from Numba JIT compilation:

Operation Typical Time Notes
bearing() ~0.5 µs Same cost as distance computation
destination() ~0.5 µs Vincenty direct converges quickly

These are fast enough for real-time applications with thousands of calls per second.

Using Different Ellipsoids

Both bearing() and destination() accept an optional ellipsoid parameter. By default, WGS-84 is used. You can choose from six built-in ellipsoids or pass a custom (a, f) tuple:

from geodistpy import bearing, destination, ELLIPSOIDS

# Bearing on the GRS-80 ellipsoid
b = bearing((52.5200, 13.4050), (48.8566, 2.3522), ellipsoid='GRS-80')
print(f"GRS-80 bearing: {b:.4f}°")

# Destination on the Airy 1830 ellipsoid
lat, lon = destination((52.5200, 13.4050), 90.0, 500, metric='km', ellipsoid='Airy (1830)')
print(f"Airy destination: ({lat:.4f}, {lon:.4f})")

# Custom ellipsoid as (semi_major_axis, flattening) tuple
lat, lon = destination((0, 0), 45.0, 1000, metric='km', ellipsoid=(6378388.0, 1/297.0))
print(f"Custom destination: ({lat:.4f}, {lon:.4f})")

# Available named ellipsoids
print(ELLIPSOIDS.keys())
# dict_keys(['WGS-84', 'GRS-80', 'Airy (1830)', 'Intl 1924', 'Clarke (1880)', 'GRS-67'])

Supported named ellipsoids: WGS-84 (default), GRS-80, Airy (1830), Intl 1924, Clarke (1880), GRS-67.