Skip to article frontmatterSkip to article content

Great Circle Arcs and Path

Authors
Affiliations
NSF National Center for Atmospheric Research

Indiana Jones Plane Flying Gif

Great Circle Arcs and Path


Overview

Imagine you are on a plane flying from Cario to Hong Kong. As a passenger, the plane appears to travel a straight path from one airport to the next. However, in reality, as the plane travels it actually curves along the surface of Earth held down by the gravity of the planet.

The “straight line” has become an arc connecting Cario and Hong Kong on a map. This arc is a great circle arc, just one small subsection of the great circle path.

  1. Distance between Points on a Great Circle Arc

  2. Convert Spherical Distance to Degrees

  3. Determine the Bearing of a Great Circle Arc

  4. Generate a Great Circle Arc with Intermediate Points

  5. Determine the Midpoint of a Great Circle Arc

  6. Generate a Great Circle Path

  7. Determine an Antipodal Point

Prerequisites

ConceptsImportanceNotes
NumpyNecessaryUsed to work with large arrays
PandasNecessaryUsed to read in and organize data (in particular dataframes)
Intro to CartopyHelpfulWill be used for adding maps to plotting
MatplotlibHelpfulWill be used for plotting
  • Time to learn: 40 minutes


Imports

To begin, let’s import the packages we will need in this notebook and collect the list of locations coordinates

import pandas as pd       # reading in data for location information from text file
import numpy as np        # working with arrays, vectors, cross/dot products, and radians

from pyproj import Geod   # working with the Earth as an ellipsod (WGS-84)
import geopy.distance     # working with the Earth as an ellipsod (WGS-84)

import matplotlib.pyplot as plt    # Plotting a figure
from cartopy import crs as ccrs, feature as cfeature  # Add World Map to Plot
# Get the coordinates for locations with all coordinates
location_df = pd.read_csv("../location_full_coords.txt")
location_df = location_df.rename(columns=lambda x: x.strip()) # strip excess white space from column names and values
location_df.head()
Loading...
# Set index to the name column, this will make it easier to access each row
location_df.index = location_df["name"]
location_df.head()
Loading...

Distance Between Points on a Great Circle Arc

We can determine the distance between two points A and B (for example, Cario and Hong Kong) both by hand mathematically and with Python packages like pyproj and geopy

Determine Distance Between Points Mathematically via Unit Sphere

Unit sphere refers to a sphere with a radius of 1, for the purpose of this cookbook, when we are using a unit sphere to represent Earth, it is a perfect sphere that has been multiplied by 6378137 meters

First, to measure the distance between point A (latA, lonA) and point B (latB, lonB) on a unit sphere (radius = 1)

d=acos(sin(latA)sin(latB)+cos(latA)cos(latB)cos(lonAlonB))d=acos(sin(latA)*sin(latB)+cos(latA)*cos(latB)*cos(lonA-lonB))

Because of the translation when working with radians and degrees, there is a mathematically equivalent formula that works better when working with shorter distances (to produce less rounding errors):

d=2asin(sin(latAlatB2)2+cos(latA)cos(latB)sin(lonAlonB2)2)d=2*asin(\sqrt{sin(\frac{latA-latB}{2})^2 + cos(latA)*cos(latB)*sin(\frac{lonA-lonB}{2})^2})

We can now translate these equations for measuring the distance between two points (the great circle arc distance) into Python and multiply the result by the Earth’s radius.

These equations use a locations’s latitude/longtiude coordinates, which we can access in a pandas dataframe with the name of the location and which coordinate type we would like, for example:

print(f"Boulder latitude: {location_df.loc["boulder", "latitude"]}")
print(f"Boulder longitude: {location_df.loc["boulder", "longitude"]}")
Boulder latitude: 40.015
Boulder longitude: -105.2705

Below, Equation 1 has become distance_between_points_default since this is the equation we will use for most equations, but Equation 2 has become distance_between_points_small for working with small distances that are less prone to rounding errors. We can call both equations to compare outputs to see how they are mathematically equivalent. By hand, Equation 1 is a faster calculation than Equation 2, but when working with Python both of these functions will run nearly as quickly.

def distance_between_points_default(start_point=None, end_point=None):
    earth_radius = 6378137  # meters
    latA = np.deg2rad(location_df.loc[start_point, "latitude"])
    lonA = np.deg2rad(location_df.loc[start_point, "longitude"])
    latB = np.deg2rad(location_df.loc[end_point, "latitude"])
    lonB = np.deg2rad(location_df.loc[end_point, "longitude"])

    distance_default = np.arccos(np.sin(latA)*np.sin(latB)+np.cos(latA)*np.cos(latB)*np.cos(lonA-lonB))
    return distance_default * earth_radius
def distance_between_points_small(start_point=None, end_point=None):
    earth_radius = 6378137  # meters
    latA = np.deg2rad(location_df.loc[start_point, "latitude"])
    lonA = np.deg2rad(location_df.loc[start_point, "longitude"])
    latB = np.deg2rad(location_df.loc[end_point, "latitude"])
    lonB = np.deg2rad(location_df.loc[end_point, "longitude"])

    distance_small = 2 * np.arcsin(np.sqrt((np.sin((latA-latB)/2))**2 + np.cos(latA)*np.cos(latB)*(np.sin((lonA-lonB)/2))**2))
    return distance_small * earth_radius

Sidenote: Additional Formulas

While we will be focusing on Equation 1 and Equation 2 for our calculations in the future, there are even more methods to calculate the distance between points on a sphere. Each formula has different degrees of precision based on which ellipsoid or sphere is used:

Determine Distance Points via Python Package pyproj

While great circle distances can be determine mathematically, there are some Python packages that account for the fact that the Earth is not a perfect sphere that will be easier to work than relying entirely on the equations derived above. For example, pyproj treats the Earth as an ellipsoid, by default specifically WGS-84.

WGS-84 is the World Geodetic System and considered to be the current standard for geodesy and working with satellite. It is an geocentric and is consistent to within 1 meter around the globe.

To begin, let use use pyproj to set up an ellipsoid (WGS-84) that will represent the Earth.

def distance_pyproj(start_pt=None, end_pt=None):
    geodesic = Geod(ellps="WGS84") # setup ellipsoid

    # Return the distance between two points (for example, Boulder and Boston)
    _, _, distance_meter =  geodesic.inv(location_df.loc[start_pt, "longitude"],
                                         location_df.loc[start_pt, "latitude"],
                                         location_df.loc[end_pt, "longitude"],
                                         location_df.loc[end_pt, "latitude"])
    
    return distance_meter
# Find the distance between Boulder and Boston
distance_meter = distance_pyproj("boulder", "boston")
print(f"Distance between coordinates (ellipsoid)   = {distance_meter/1000} km")
Distance between coordinates (ellipsoid)   = 2862.5974799145215 km

That is much easier! We can compare pyproj to the output from the equations (1 and 2) that we calculated above:

distance_unit_sphere_default = distance_between_points_default("boulder", "boston")
print(f"Distance between coordinates (unit sphere) = {distance_unit_sphere_default/1000} km")
distance_unit_sphere_small = distance_between_points_small("boulder", "boston")
print(f"Distance between coordinates (unit sphere) = {distance_unit_sphere_small/1000} km")
Distance between coordinates (unit sphere) = 2858.532213639344 km
Distance between coordinates (unit sphere) = 2858.5322136393447 km

Both equations we calculated above slightly underestimate the distance between Boston and Boulder because they assume the Earth is a perfect sphere, pyproj accounts for ellipse.

Convert Spherical Distance to Degrees

Sometimes when you are working with a map, you don’t always just need a distance in meters or kilometers. We can also convert the distances calculated back into degrees as needed. To convert a distance from meters to degrees, we will assume that the great circle is on a unit sphere with a constant spherical radius of ~6371 km (mean radius of Earth).

Side note: The Python library ObsPy has a similar function built-in: ObsPy kilometer2degrees()

earth_radius = 6378.137 # km

def km_to_degree_distance(distance_km=None):
    return distance_km / (2 * earth_radius * np.pi / 360)

def degree_to_km_distance(distance_degree=None):
    return distance_degree * (2 * earth_radius * np.pi / 360)
# We can convert back and form between kilometers and degrees
print(f"300 km to degrees = {km_to_degree_distance(300)} degrees")
print(f"2.6949458523585643 degree to km = {degree_to_km_distance(2.6949458523585643)} km")
300 km to degrees = 2.6949458523585643 degrees
2.6949458523585643 degree to km = 300.0 km

Determine the Bearing of a Great Circle Arc

Arcs are not just a distance, but a direction. Now, we can calculate the direction that an arc takes as it moves from Point A to Point B also known as the bearing.

Determine the Bearing Mathematically via Unit Sphere

The bearing between between Point A (latA, lonA) and Point B (latB, lonB) on a unit sphere can be calculated as:

x=cos(latA)sin(latB)sin(latA)cos(latB)cos(lonBlonA)x = cos(latA) * sin(latB) - sin(latA) * cos(latB) * cos(lonB - lonA)
y=sin(lonBlonA)cos(latB)y = sin(lonB - lonA) * cos(latB)
θ=atan2(y,x)θ = atan2(y, x)

This can be similarly converted to Python as:

def bearing_between_points_unit_sphere(start_point=None, end_point=None):
    latA = np.deg2rad(location_df.loc[start_point, "latitude"])
    lonA = np.deg2rad(location_df.loc[start_point, "longitude"])
    latB = np.deg2rad(location_df.loc[end_point, "latitude"])
    lonB = np.deg2rad(location_df.loc[end_point, "longitude"])

    x = np.cos(latA) * np.sin(latB) - np.sin(latA) * np.cos(latB) * np.cos(lonB - lonA)
    y = np.sin(lonB - lonA) * np.cos(latB)
    bearing = np.arctan2(y, x)
    return np.rad2deg(bearing) % 360

Determine the Bearing via Python Package pyproj

pyproj can also use the same geodesic.inv() function to calculate bearing. However, pyproj includes both the forward and reverse bearing. Forward bearing refers to the bearing from Point A to Point B, while the reverse bearing is the bearing from Point B to Point A. The reverse bearing is returned as a negative angle, but it can also be calculated as the Forward Bearing - 360.

For example, to find the bearing from Boulder and Boston:

geodesic = Geod(ellps="WGS84")
fwd_bearing, rvs_bearing, _ =  geodesic.inv(location_df.loc["boulder", "longitude"],
                                             location_df.loc["boulder", "latitude"],
                                             location_df.loc["boston", "longitude"],
                                             location_df.loc["boston", "latitude"])
print(f"Forward Bearing: {fwd_bearing}")
print(f"Reverse Bearing: {rvs_bearing} ({rvs_bearing%360})")
Forward Bearing: 73.51048829569022
Reverse Bearing: -83.57035585674933 (276.4296441432507)

When referring to bearing however, it is typically assumed to be referring to the forward bearing, so we will use the function below as we move forward.

def bearing_between_points_ellps(start_point=None, end_point=None):
    geodesic = Geod(ellps="WGS84")
    fwd_bearing, _, _ =  geodesic.inv(location_df.loc[start_point, "longitude"],
                                        location_df.loc[start_point, "latitude"],
                                        location_df.loc[end_point, "longitude"],
                                        location_df.loc[end_point, "latitude"])
    return fwd_bearing

Because of the slightly flattened shape of an ellipsoid compared to a sphere, the bearing can also vary depending on if you are calculating bearing on a unit sphere or a ellipsoid:

### Compare Unit Sphere and Ellipsoid
beaing_ellps = bearing_between_points_ellps("boulder", "boston")
print(f"forward bearing between coordinates (ellipsoid)   = {beaing_ellps} Degrees")
bearing_us = bearing_between_points_unit_sphere("boulder", "boston")
print(f"forward bearing between coordinates (unit sphere) = {bearing_us} Degrees")
forward bearing between coordinates (ellipsoid)   = 73.51048829569022 Degrees
forward bearing between coordinates (unit sphere) = 73.49180375272644 Degrees

Generating a Great Circle Arc with Intermediates Points

Now we have calculated both the length of a great circle arc as well as its bearing. Now we can begin to add additional points along the arc. These intermediate points will lie along the great circle arc and can be calculated as the fractional distance laong the arc (for example, a point for every 10th of the arc).

Determine Intermediate Points Mathemetically via Unit Sphere (Fractional Distance)

Along a unit sphere, we can calculate each intermediate point as a fractional distance along the arc. We can determine the points as a given fraction of a distance (d) between a starting points A (latA, lonA) and the final point B (latB, lonB) where f is a fraction along the great circle arc. As a result, Point A is atf=0 and Point B sits atf=1 along the arc.

Note: The points must not lie on exact opposite sides of a sphere (antipodal), otherwise, the path is undefined because f=0=1

Where, antipodal is defined by:

latA+latB=0latA + latB = 0

abs(lonAlonB)=piabs(lonA - lonB) = pi

Where the distance between two points is the angular distance:

d=total distance of arcearth’s radiusd = \frac{\text{total distance of arc}}{\text{earth's radius}}

The intermediate points (lat, lon) along a given path starting point to end point:

A=sin((1f)dsin(d)A = sin(\frac{(1-f) * d}{sin(d)}
B=sin(fd)sin(d)B = \frac{sin(f*d)}{sin(d)}
x=Acos(latA)cos(lonA)+Bcos(latB)cos(lonB)x = A * cos(latA) * cos(lonA) + B * cos(latB) * cos(lonB)
y=Acos(latA)sin(lonA)+Bcos(latB)sin(lonB)y = A * cos(latA) * sin(lonA) + B * cos(latB) * sin(lonB)
z=Asin(latA)+Bsin(latB)z = A * sin(latA) + B * sin(latB)
lat=atan2(z,x2+y2)lat = atan2(z, \sqrt{x^2 + y^2})
lon=atan2(y,x)lon = atan2(y, x)

Let’s translate this math into Python code:

def intermediate_points(start_point=None, end_point=None,
                        fraction=None, distance=None):
    # Calculates the position of an intermediate point based on its fractional position along an arc defined from Point A to Point B
    earth_radius = 6378137  # meters
    total_distance = distance / earth_radius
    latA = np.deg2rad(location_df.loc[start_point, "latitude"])
    lonA = np.deg2rad(location_df.loc[start_point, "longitude"])
    latB = np.deg2rad(location_df.loc[end_point, "latitude"])
    lonB = np.deg2rad(location_df.loc[end_point, "longitude"])

    A = np.sin((1-fraction) * total_distance) / np.sin(total_distance)
    B = np.sin(fraction * total_distance) / np.sin(total_distance)
    x = (A * np.cos(latA) * np.cos(lonA)) + (B * np.cos(latB) * np.cos(lonB))
    y = (A * np.cos(latA) * np.sin(lonA)) + (B * np.cos(latB) * np.sin(lonB))
    z = (A * np.sin(latA)) + (B * np.sin(latB))
    lat = np.arctan2(z, np.sqrt(x**2 + y**2))
    lon = np.arctan2(y, x)
    return (float(np.rad2deg(lat)), float(np.rad2deg(lon)))

def calculate_intermediate_pts(start_point=None, end_point=None,
                               fraction=None, total_distance_meter=None):
    # returns a list of intermediate points along the arc separated by a specific fraction
    fractions = np.arange(0, 1+fraction, fraction)
    intermediate_lat_lon = []
    for fractional in fractions:
        intermediate_pts = intermediate_points(start_point, end_point,
                                                fractional, total_distance_meter)
        intermediate_lat_lon.append(intermediate_pts)
    return intermediate_lat_lon

Determine Intermediate Points via Python Package pyproj and geopy

However, there is a strong advantage when you the Python libraries pyproj and geopy. Not only can you use an ellipsoid instead of a sphere, you can also interpolate points along an arc in additional ways:

  • A fractional distance laong the arc (for example, a point for every 10th of the arc)

  • A known distance along the arc (for example, a point ever 100 meters)

  • An equal amount of points (for example, adding 6 intermediate points which are equally spaced along the arc)

Below you will see how we can use one function interpolate_points_along_gc to be able to solve for all three possible types of intermediate points. The function will use both the starting and ending coordinates (Point A to Point B) and a distance you’d like to have between each point.

def interpolate_points_along_gc(lat_start,
                                lon_start,
                                lat_end,
                                lon_end,
                                distance_between_points_meter): 
    # Interpolate intermediate points along an arc with a given distance between each point
    lat_lon_points = [(float(lat_start), float(lon_start))]
    
    # move to next point when distance between points is less than the equal distance
    move_to_next_point = True
    while(move_to_next_point):
        forward_bearing, _, distance_meters = geodesic.inv(lon_start,
                                                            lat_start, 
                                                            lon_end,
                                                            lat_end)
        if distance_meters < distance_between_points_meter:
            # ends before overshooting
            move_to_next_point = False
        else:
            start_point = geopy.Point(lat_start, lon_start)
            distance_to_move = geopy.distance.distance(
                            kilometers=distance_between_points_meter /
                            1000)  # distance to move towards the next point
            final_position = distance_to_move.destination(
                            start_point, bearing=forward_bearing)
            lat_lon_points.append((float(final_position.latitude), float(final_position.longitude)))
            # new starting position is newly found end position
            lon_start, lat_start = final_position.longitude, final_position.latitude
    lat_lon_points.append((float(lat_end), float(lon_end)))
    return lat_lon_points

Interpolate with N Total Equally Spaced Points

Imagine we are flying a plane with a set amount of gas and will need to make 10 stops along our path to refuel from Boulder to Boston. If we need to stop 10 times (imagine that there is a refueling station wherever we stop) then we can interpolate along our path with 10 additional points

n_total_points = 10 # total points (n points)

# Calculate the distance between Point A (Boulder) and Point B (Boston)
distance_meter = distance_pyproj("boulder", "boston")
print(f"Total distance from Boulder to Boston = {distance_meter/1000} km")

distance_between_points_meter = distance_meter / (n_total_points + 1)
print(f"Each of the {n_total_points} points will be separated by {distance_between_points_meter} meters ({distance_between_points_meter/1000} km)")
Total distance from Boulder to Boston = 2862.5974799145215 km
Each of the 10 points will be separated by 260236.13453768377 meters (260.23613453768377 km)

Excellent! Now that we know the distances between each of the 10 equally spaced points we can use interpolate_points_along_gc to find a list of interpolated points. Note that will be 12 total points, 10 intermediate points and 1 for each the starting and ending position.

lat_start, lon_start = location_df.loc[["boulder"]]["latitude"].iloc[0], location_df.loc[["boulder"]]["longitude"].iloc[0]
lat_end, lon_end = location_df.loc[["boston"]]["latitude"].iloc[0], location_df.loc[["boston"]]["longitude"].iloc[0]

interpolated_pts = interpolate_points_along_gc(lat_start,
                                          lon_start,
                                          lat_end,
                                          lon_end,
                                          distance_between_points_meter)
print(f"{len(interpolated_pts)} Total Points")
print(f"List of Interpolated Points:\n{interpolated_pts}")
12 Total Points
List of Interpolated Points:
[(40.015, -105.2705), (40.64283438472448, -102.32002071588883), (41.19386139956729, -99.31719425393653), (41.665293789240074, -96.2672998277903), (42.05464865958041, -93.17653047007545), (42.35980367525435, -90.05192021556942), (42.579048241302566, -86.9012334462751), (42.71112689737456, -83.73281874084786), (42.75527239726804, -80.5554326250441), (42.711226442193585, -77.37804142647055), (42.579246749547636, -74.20961159223961), (42.3601, -71.0589)]

Plot Arcs as Points on a World Map

The easiest way to understand how these interpolated points will look along an arc is to plot them. To be able to reuse this plotting structure in multiple examples below we will encompass the plot into a simple function which takes in a string for the plot’s title and a list of coordinates (which will include Point A at the start and Point B at the end)

def plot_coordinate(lst_of_coords=None, title=None):
    # Set up world map plot on the United States
    fig = plt.subplots(figsize=(15, 10))
    projection_map = ccrs.PlateCarree()
    ax = plt.axes(projection=projection_map)
    lon_west, lon_east, lat_south, lat_north = -130, -60, 20, 60
    ax.set_extent([lon_west, lon_east, lat_south, lat_north], crs=projection_map)
    ax.coastlines(color="black")
    ax.add_feature(cfeature.STATES, edgecolor="black")
    
    # Plot Latitude/Longitude Coordinates
    longitudes = [x[1] for x in lst_of_coords] # longitude
    latitudes = [x[0] for x in lst_of_coords] # latitude
    plt.plot(longitudes, latitudes)
    plt.scatter(longitudes, latitudes)
    
    # Setup Axis Limits and Title/Labels
    plt.title(title)
    plt.show()

Now, let’s plot those 10 equally spaced points!

plot_coordinate(interpolated_pts,
                title=f"Interpolate {n_total_points} Equally Spaced Points")
/home/runner/micromamba/envs/cookbook-gc/lib/python3.13/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/50m_physical/ne_50m_coastline.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
/home/runner/micromamba/envs/cookbook-gc/lib/python3.13/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/50m_cultural/ne_50m_admin_1_states_provinces_lakes.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
<Figure size 1500x1000 with 2 Axes>

Looking good! Alright, let’s move along to interpolate based on a known distance

Interpolate every N meters

A plane has a fuel tank that can hold a known amount of gas, what if instead we wanted to known how far we need to refuel, but now in meters? Let’s say we hold a gas tank that can hold enough fuel to get 112 km, how many times will we need to stop and where along would those stops appear?

distance_between_points_meter = 112000 # 122.0 km
print(f"Each point will be separated by {distance_between_points_meter} meters ({distance_between_points_meter/1000} km)")
Each point will be separated by 112000 meters (112.0 km)
lat_start, lon_start = location_df.loc["boulder", "latitude"], location_df.loc["boulder", "longitude"]
lat_end, lon_end = location_df.loc["boston", "latitude"], location_df.loc["boston", "longitude"]

interpolated_pts = interpolate_points_along_gc(lat_start,
                                          lon_start,
                                          lat_end,
                                          lon_end,
                                          distance_between_points_meter)
print(f"{len(interpolated_pts)} Total Points")
print(f"List of Interpolated Points:\n{interpolated_pts}")
27 Total Points
List of Interpolated Points:
[(40.015, -105.2705), (40.29443152420481, -104.00739372929635), (40.55994883031889, -102.73410083358552), (40.811312209497714, -101.45097760323463), (41.04829010633232, -100.15841313140038), (41.27065999040772, -98.8568290209118), (41.47820922569627, -97.54667886388195), (41.67073593071231, -96.22844748540258), (41.848049822017195, -94.90264994540797), (42.00997303342117, -93.56983029584387), (42.156340903095064, -92.23056009359964), (42.287002720790326, -90.88543667319323), (42.40182242747838, -89.53508118686946), (42.50067925996674, -88.1801364234981), (42.583468333429785, -86.82126442135142), (42.65010115530707, -85.45914389340653), (42.700506064664665, -84.09446748716023), (42.73462859187762, -82.72793890397021), (42.75243173435998, -81.36026990555973), (42.75389614502746, -79.99217723746453), (42.73902023120852, -78.62437950079696), (42.707820162798164, -77.25759400470386), (42.66032978955507, -75.89253363227026), (42.596600468550456, -74.52990375235937), (42.51670080386281, -73.1703992089883), (42.42071630165334, -71.81470141834599), (42.3601, -71.0589)]
plot_coordinate(interpolated_pts,
                title=f"Interpolate every {distance_between_points_meter/1000} km")
<Figure size 1500x1000 with 2 Axes>

That’s a lot of stop! Ok, now we can return to a familiar system. We can use this same system to also determine fractional distances as well!

Interpolate a fractional distance along arc

Above we calculated how to find intermediate points as a fractional distance along an arc. We can now return to this, but with the advantages of using an ellipsoid! Let’s try this method and interpolate points every 10th of an arc.

fraction = 1/10
distance_between_points_meter = fraction * distance_meter
print(f"Interpolate for every {fraction} of the arc")
print(f"Each point will be separated by {distance_between_points_meter} meters ({distance_between_points_meter/1000} km)")
Interpolate for every 0.1 of the arc
Each point will be separated by 286259.74799145217 meters (286.2597479914522 km)
lat_start, lon_start = location_df.loc["boulder", "latitude"], location_df.loc["boulder", "longitude"]
lat_end, lon_end = location_df.loc["boston", "latitude"], location_df.loc["boston", "longitude"]

interpolated_pts = interpolate_points_along_gc(lat_start,
                                          lon_start,
                                          lat_end,
                                          lon_end,
                                          distance_between_points_meter)
print(f"{len(interpolated_pts)} Total Points")
print(f"List of Interpolated Points:\n{interpolated_pts}")
11 Total Points
List of Interpolated Points:
[(40.015, -105.2705), (40.70144152851926, -102.0220139666611), (41.29459964930597, -98.7107954391427), (41.790822409112124, -95.34405626799516), (42.18692017366623, -91.93032741511365), (42.4802543416051, -88.47932636547252), (42.66881568690329, -85.00175846666659), (42.75128706952139, -81.50905885116296), (42.72708599453554, -78.01308797522954), (42.59638380227174, -74.52579917182065), (42.3601, -71.0589)]
plot_coordinate(interpolated_pts,
                title=f"(Ellipsoid) Interpolate every {fraction}th of the arc")
<Figure size 1500x1000 with 2 Axes>

We can now compare both the ellipsoid and spherical model. First, let’s plot the same fractional distance along the unit sphere:

distance_unit_sphere_default = distance_between_points_default("boulder", "boston")
intermediate_unit_sphere = calculate_intermediate_pts("boulder", "boston",
                                               fraction, distance_unit_sphere_default)
print(f"{len(intermediate_unit_sphere)} Total Points")
print(f"List of Interpolated Points:\n{intermediate_unit_sphere}")
11 Total Points
List of Interpolated Points:
[(40.015, -105.2705), (40.69956840796515, -102.0224051615289), (41.291305596308824, -98.71150732723615), (41.786544424077, -95.3450112546059), (42.1820804843035, -91.93144054027105), (42.475261312703864, -88.48050569778584), (42.66406520473303, -85.00290620334833), (42.74716436193266, -81.51007310432796), (42.723967826794556, -78.01386515980239), (42.59464096704978, -74.52623685097386), (42.3601, -71.0589)]
plot_coordinate(intermediate_unit_sphere,
                title=f"(Unit Sphere) Interpolate every {fraction}th of the arc")
<Figure size 1500x1000 with 2 Axes>

The difference between the ellipsoid and the sphere will become more apparently as intermediate points along the arc are shifted to new positions based on whether or not it is a sphere.

# Compare Unit Sphere and Ellipsoid
for i in range(len(interpolated_pts)):
    _, _, distance_m = geodesic.inv(interpolated_pts[i][0], interpolated_pts[i][1],
                                   intermediate_unit_sphere[i][0], intermediate_unit_sphere[i][1])
    if np.isnan(distance_m): distance_m = 0
    print(f"Difference in distance between ellipsoid/unit sphere at point {i}: {distance_m} meters")
Difference in distance between ellipsoid/unit sphere at point 0: 0 meters
Difference in distance between ellipsoid/unit sphere at point 1: 0 meters
Difference in distance between ellipsoid/unit sphere at point 2: 0 meters
Difference in distance between ellipsoid/unit sphere at point 3: 0 meters
Difference in distance between ellipsoid/unit sphere at point 4: 0 meters
Difference in distance between ellipsoid/unit sphere at point 5: 132.55154860348821 meters
Difference in distance between ellipsoid/unit sphere at point 6: 136.26443374199349 meters
Difference in distance between ellipsoid/unit sphere at point 7: 132.0972186842191 meters
Difference in distance between ellipsoid/unit sphere at point 8: 112.95654352480192 meters
Difference in distance between ellipsoid/unit sphere at point 9: 71.29186326482345 meters
Difference in distance between ellipsoid/unit sphere at point 10: 0.0 meters

Determine the Midpoint of a Great Circle Arc

The advantage of using fractional distances it is make it simple to find the midpoint along the arc. The midpoint of an arc can be determined as a fractional distance along an arc where f = 0.5.

distance_meter = distance_pyproj("boulder", "boston")

# Distance along the arc where the midpoint lies:
midpoint = distance_meter / 2
lat_start, lon_start = location_df.loc["boulder", "latitude"], location_df.loc["boulder", "longitude"]
lat_end, lon_end = location_df.loc["boston", "latitude"], location_df.loc["boston", "longitude"]

intermediate_geodesic = interpolate_points_along_gc(lat_start,
                                          lon_start,
                                          lat_end,
                                          lon_end,
                                          midpoint)
print(f"{len(interpolated_pts)} Total Points")
print(f"List of Interpolated Points:\n{interpolated_pts}")
print(f"Midpoint = {interpolated_pts[1]}")
11 Total Points
List of Interpolated Points:
[(40.015, -105.2705), (40.70144152851926, -102.0220139666611), (41.29459964930597, -98.7107954391427), (41.790822409112124, -95.34405626799516), (42.18692017366623, -91.93032741511365), (42.4802543416051, -88.47932636547252), (42.66881568690329, -85.00175846666659), (42.75128706952139, -81.50905885116296), (42.72708599453554, -78.01308797522954), (42.59638380227174, -74.52579917182065), (42.3601, -71.0589)]
Midpoint = (40.70144152851926, -102.0220139666611)

Ok, let’s compare the midpoint along the ellipsoid and the unit sphere:

distance_unit_sphere_default = distance_between_points_default("boulder", "boston")
intermediate_unit_sphere = calculate_intermediate_pts("boulder", "boston",
                                               1/2, distance_unit_sphere_default)
print(f"{len(intermediate_unit_sphere)} Total Points")
print(intermediate_unit_sphere)
print(f"Midpoint = {intermediate_geodesic[1]}")
3 Total Points
[(40.015, -105.2705), (42.475261312703864, -88.48050569778584), (42.3601, -71.0589)]
Midpoint = (42.48025434160511, -88.4793263654725)
# Compare geodesic and unit sphere
_, _, distance_m = geodesic.inv(intermediate_geodesic[1][0], intermediate_geodesic[1][1],
                                   intermediate_unit_sphere[1][0], intermediate_unit_sphere[1][1])
print(f"Distance between unit sphere and ellipsoid's midpoints = {distance_m} meters")
Distance between unit sphere and ellipsoid's midpoints = 132.55154860505925 meters

132 meters isn’t very far when you are working with something as large as the Earth, but that is more than an entire football field away from each other!

Generate a Great Circle Path

A great circle arc is the arc formed between two points along the surface of a sphere. So far, this is all we’ve focused on. However, imagine if you extending that arc in the same bearing all the way around the globe until you returned back to your starting position. This is known as a great circle path.

The great circle path can be used to determine where (at what latitude/longtiude) a great circle would cross a given parallel as well as illustrate how the plane cutting through the globe that is used to form a great circle.

Because a great circle path will move from -180 degrees to 180 degrees longitude, then all we need to know is what the latitude at each point would be. A a valid great circle path only exists if:

  • Point A and Point B are not meridians (at the same longitude, directly above or below each other)

  • Point A and Point B are not Antipodal (exactly on opposite sides of the globe)

Otherwise, the great circle path is considered undefined.

To do this, we will generate a list of longitude points from -180 to 180 degrees. By default, this will be 360 total points, 1 point for each degree longitude. Then, since we know the starting and ending point, we can determine the latitude for any longitude point (lon) along the great circle path generated from a great circle arc (from Point A to Point B):

latitude=atan(sin(latA)cos(latB)sin(lonlonB)sin(latB)cos(latA)sin(lonlon1)cos(latA)cos(latB)sin(lonAlonB))\text{latitude} = atan(\frac{sin(latA)*cos(latB)*sin(lon-lonB)-sin(latB)*cos(latA)*sin(lon-lon1)}{cos(latA)*cos(latB)*sin(lonA-lonB)})

See more: Latitude of point on GC

# Find Latitude Coordinate based on a Longitude Coordinate and a Great Circle Arc formed by Point A (start) to Point B (end)
def generate_latitude_along_gc(start_point=None, end_point=None, number_of_lon_pts=360):
    # Generate a list of latitude coordinates for the great circle path

    # First convert each point to radians (to work with sin/cos)
    lat1 = np.deg2rad(location_df.loc[start_point, "latitude"])
    lon1 = np.deg2rad(location_df.loc[start_point, "longitude"])
    lat2 = np.deg2rad(location_df.loc[end_point, "latitude"])
    lon2 = np.deg2rad(location_df.loc[end_point, "longitude"])

    # Verify not meridian (longitude passes through the poles)
    # If points lie at the same longitude, alert user and return a totally linear path
    if np.sin(lon1 - lon2) == 0:
        print("Invalid inputs: start/end points are meridians")
        # plotting meridians at 0 longitude through all latitudes
        meridian_lat = np.arange(-90, 90, 180/len(longitude_lst)) # split in n number
        meridians = []
        for lat in meridian_lat:
            meridians.append((lat, 0))
        return meridians

    # verify not antipodal (diametrically opposite points)
    if lat1 + lat2 == 0 and abs(lon1-lon2) == np.pi:
        # if points are anitpodal, alert user and return empty list
        print("Invalid inputs: start/end points are antipodal")
        return []

    # generate N total number of longitude points along the great circle
    # Similar code in R: https://github.com/rspatial/geosphere/blob/master/R/greatCircle.R#L18C3-L18C7
    gc_lon_lst = []
    for lon in range(1, number_of_lon_pts+1):
        new_lon = (lon  * (360/number_of_lon_pts) - 180)
        gc_lon_lst.append(np.deg2rad(new_lon))

    # Intermediate points on a great circle: https://edwilliams.org/avform147.htm#Int
    # Equation 22
    gc_lat_lon = []
    for gc_lon in gc_lon_lst:
        num = np.sin(lat1)*np.cos(lat2)*np.sin(gc_lon-lon2)-np.sin(lat2)*np.cos(lat1)*np.sin(gc_lon-lon1)
        den = np.cos(lat1)*np.cos(lat2)*np.sin(lon1-lon2)
        new_lat = np.arctan(num/den)
        # convert back to degrees and save latitude/longitude pair
        gc_lat_lon.append((np.rad2deg(new_lat), np.rad2deg(gc_lon)))
    return gc_lat_lon

Now we have a great circle arc (formed from Point A to Point B) and a list of possible longitude values (from -180 to 180 degrees). With generate_latitude_along_gc we can generate a full list of latitude/longitude pairs for the entire great circle path!

To plot both the great circle arc and the great circle path, we will use arc_points to get a list of the great circle arc positions and generate_latitude_along_gc to get a list of the points along the great circle path. There will be obvious overlap for the small section of the path that contains the arc.

def arc_points(start_lat=None,
               start_lon=None,
               end_lat=None,
               end_lon=None,
               n_total_points=10):
    _, _, distance_meter =  geodesic.inv(start_lon,
                                        start_lat,
                                        end_lon,
                                        end_lat)
        
    distance_between_points_meter = distance_meter / (n_total_points + 1)

    
    points_along_arc = interpolate_points_along_gc(start_lat,
                                              start_lon,
                                              end_lat,
                                              end_lon,
                                              distance_between_points_meter)
    return points_along_arc

Time to plot! To keep it simple plot_coordinates will take in a list of points along the great circle path with a starting and ending point (Point A and B) and internally generate a list of points for the great circle arc.

def plot_coordinate(lat_lon_lst=None,
                    start_point=None, end_point=None,
                    title=None):
    # Set up world map plot
    fig = plt.subplots(figsize=(15, 10))
    projection_map = ccrs.PlateCarree()
    ax = plt.axes(projection=projection_map)
    lon_west, lon_east, lat_south, lat_north = -180, 180, -90, 90
    ax.set_extent([lon_west, lon_east, lat_south, lat_north], crs=projection_map)
    ax.coastlines(color="black")
    ax.add_feature(cfeature.BORDERS, edgecolor='grey')
    ax.add_feature(cfeature.STATES, edgecolor="grey")
        
    # Get list of points along the great circle path (in blue)
    longitudes = [x[1] for x in lat_lon_lst] # longitude
    latitudes = [x[0] for x in lat_lon_lst] # latitude
    plt.plot(longitudes, latitudes, c="cornflowerblue")
    plt.scatter(longitudes, latitudes, c="cornflowerblue")

    # Generate a great circle arc from the starting to ending point (in red)
    start_end_lat_lon = arc_points(location_df.loc[start_point, "latitude"],
                                   location_df.loc[start_point, "longitude"],
                                   location_df.loc[end_point, "latitude"],
                                   location_df.loc[end_point, "longitude"],
                                   n_total_points=20)
    longitudes = [x[1] for x in start_end_lat_lon] # longitude
    latitudes = [x[0] for x in start_end_lat_lon] # latitude
    plt.plot(longitudes, latitudes, c="red")
    plt.scatter(longitudes, latitudes, c="red")
    
    # Setup Axis Limits and Title/Labels
    plt.title(title)
    plt.show()

Let’s see how this look! First, let’s see how the great circle path and arc from Boulder to Boston looks:

start_pt = "boulder"
end_pt = "boston"
n_pts = 360 # number of points along the longitude (resolution for 360 is one point for each 1 degree)
lat_lon_pts = generate_latitude_along_gc(start_pt, end_pt, number_of_lon_pts=n_pts)
plot_coordinate(lat_lon_pts, start_pt, end_pt,
                f"Plot Great Circle, made from the arc {start_pt.title()} to {end_pt.title()}")
<Figure size 1500x1000 with 2 Axes>

How about a longer arc? Let’s start at the Arceibo Telescope and move up to Greenwich (the Prime Meridian)

start_pt = "arecibo"
end_pt = "greenwich"
n_pts = 360 # number of points along the longitude (resolution for 360 is one point for each 1 degree)
lat_lon_pts = generate_latitude_along_gc(start_pt, end_pt, number_of_lon_pts=n_pts)
plot_coordinate(lat_lon_pts, start_pt, end_pt,
                f"Plot Great Circle, made from the arc {start_pt.title()} to {end_pt.title()}")
<Figure size 1500x1000 with 2 Axes>

Excellent! And now a very extreme example, starting all the way down in Zambezi and all the way up to Svalbard

start_pt = "zambezi"
end_pt = "svalbard"
n_pts = 360
lat_lon_pts = generate_latitude_along_gc(start_pt, end_pt, number_of_lon_pts=n_pts)
plot_coordinate(lat_lon_pts, start_pt, end_pt,
                f"Plot Great Circle, made from the arc {start_pt.title()} to {end_pt.title()}")
<Figure size 1500x1000 with 2 Axes>

Wait! Wasn’t a Great Circle Suppose to be a Line?

You are right! Great circle paths will appear like a straight line that cuts the globe in half. However, on a flat projection, a perfectly straight path will appear curved.

Determine an Antipodal Point

Imagine you could directly down below your feet through the Earth and all the way to the otherside of the planet. Where would that be? This exactly opposite position is known as a antipodal point.

The latitude is simple, it is simply the inverse of your current latitude. So, standing at 40 degrees in Boulder, my antipodal latitude will sit at -40 degrees, we can defined this as:

antipodal latitude=1latitude\text{antipodal latitude} = -1 * \text{latitude}

Antipodal longitude is a little more complicated. This will depend on if you are standing to the East (positive) or West (negative) of the Prime Meridian (0 degrees longitude)

anitpodal longitude=(longitude+180) if longitude0\text{anitpodal longitude} = (\text{longitude} + 180) \text{ if longitude} \le 0
anitpodal longitude=(longitude180) if longitude>0\text{anitpodal longitude} = (\text{longitude} - 180) \text{ if longitude} \gt 0
def antipodal(start_point=None):
    # return the antipodal (latitude/longitude) point from a starting position
    anti_lat = -1 * location_df.loc[start_point, "latitude"]
    ref_lon = location_df.loc[start_point, "longitude"]
    if ref_lon > 0:
        anti_lon = ref_lon - 180
    else:
        anti_lon = ref_lon + 180
    return (float(anti_lat), float(anti_lon))
def is_antipodal(start_point=None, end_point=None):
    # return true is a point is antipodal from another point
    lon1 = np.deg2rad(location_df.loc[start_point, "longitude"])
    lat1 = np.deg2rad(location_df.loc[start_point, "latitude"])
    lon2 = np.deg2rad(location_df.loc[end_point, "longitude"])
    lat2 = np.deg2rad(location_df.loc[end_point, "latitude"])
    return lat1 + lat2 == 0 and abs(lon1-lon2) == np.pi

See for yourself on this antipodes map, but we can also plot this in Python.

def plot_antipodal(start_point=None):
    # Set up world map plot
    fig = plt.subplots(figsize=(15, 10))
    projection_map = ccrs.PlateCarree()
    ax = plt.axes(projection=projection_map)
    lon_west, lon_east, lat_south, lat_north = -180, 180, -90, 90
    ax.set_extent([lon_west, lon_east, lat_south, lat_north], crs=projection_map)
    ax.coastlines(color="black")
    ax.add_feature(cfeature.BORDERS, edgecolor='grey')
    ax.add_feature(cfeature.STATES, edgecolor="grey")
        
    # Plot Start point (in blue)
    plt.scatter(location_df.loc[start_point, "longitude"],
                location_df.loc[start_point, "latitude"],
                s=100, c="cornflowerblue", label=start_point.title())

    # Plot Antipodal Point (in red)
    antipodal_point = antipodal(start_point)
    plt.scatter(antipodal_point[1], antipodal_point[0], s=100, c="red", label="Antipodal")
    
    # Setup Axis Limits and Title/Labels
    plt.title(f"{start_point.title()} and Antipodal Point {antipodal_point}")
    plt.legend(loc="lower right")
    plt.show()

Let’s see how this looks. Where does the antipodal point from Boulder lie?

plot_antipodal("boulder")
<Figure size 1500x1000 with 2 Axes>

Ah, the middle of the ocean. What about Svalbard?

plot_antipodal("svalbard")
<Figure size 1500x1000 with 2 Axes>

Just off the coast of Antartica! And the city of Cairo?

plot_antipodal("cairo")
<Figure size 1500x1000 with 2 Axes>

We are back in the ocean, but that isn’t strange. The Earth is mostly water, so don’t be surprised if the point on the opposite of the globe from you is lying in the middle of the ocean.


Summary

This notebook covered how to generate and plot great circle arcs and paths with just a starting and ending position. This also included how to calculate the bearing of a great circle arc as well interpolating along the great circle and even how to find the antipodal point to any point on the globe. You were able to see how the difference between treating the Earth as a sphere or as an ellipsoid impacts the final precision.

What’s next?

With a great circle arc defined, it’s time to start using the great circle arc as part of a large question. Next, we will determine if a third point is along the arc or at what distance it sits from the great circle arc and path.

Resources and references