Here are a selection of distance calculation methods, along with some example code at the bottom to demonstrate the different measurements for the length of the border between USA and Mexico.
For more information on the application of Pythagoras’ Theorem to the calculation of distances in projected cooordiates, see the trigonometry page.
import sys
from pyproj import Geod
from math import sqrt, atan2, cos, hypot, sin, tan, radians, acos
''' CALCULATING DISTANCE ON A FLAT (PLANAR) SURFACE'''
def planar_hypotenuse_distance(x1, y1, x2, y2):
"""
Simple Pythagoras' Theorem approach to finding the hypotenuse
(x2,y2) +
/|
a / | b
/ |
(x1,y1) +---+
c
In a right angled triangle, the length of the hypotenuse is equal to the
square root of the sum of the squares of the other two sides.
(a-squared = b-squared + c-squared)
This implementation uses no functions from the math library
(Square root is raising to the power of 0.5)
You can alternatively use the built-in sqrt() and pow() functions:
`return sqrt(pow(x1-x2,2) + pow(y1-y2,2))`
"""
return ((x1-x2)**2 + (y1-y2)**2)**0.5
def planar_hypotenuse_distance_2(x1, y1, x2, y2):
"""
Simple Pythagoras' Theorem approach to finding the hypotenuse, using the
implementation built in to the Python math library
"""
return hypot(x1-x2, y1-y2)
''' CALCULATING DISTANCE ON A SPHERE'''
# Average radius of WGS84 spherical Earth model in m
R = 6371008.771415
def spherical_equirectangular_distance(x1, y1, x2, y2):
"""
Simple Pythagoras' Theorem approach to finding the hypotenuse with an equirectangular
projection
This is only an approximation, and should not be used under most circumstances
"""
# convert to radians
y1 = radians(y1)
x1 = radians(x1)
y2 = radians(y2)
x2 = radians(x2)
# pythagoras
x = (x2-x1) * cos((y1+y2) / 2)
y = (y2-y1)
return sqrt(x**2 + y**2) * R
def spherical_haversine_distance(x1, y1, x2, y2):
"""
Calculate the 'as the crow flies' distance between two locations along a sphere using
the Haversine equation. (HAVERSINE = HAlf inVERse SINE)
This is a sligltly more complex approach than spherical_cosine_distance() and is more
precise. This is the one most commonly used in normal circumstances.
"""
# convert to radians (as this is what the math functions expect)
a1 = radians(y1)
b1 = radians(x1)
a2 = radians(y2)
b2 = radians(x2)
# half sine
sa = sin((a2 - a1)*0.5)
sb = sin((b2 - b1)*0.5)
# the square of half the chord length between the points
a = sa * sa + cos(a1) * cos(a2) * sb * sb
# angular distance in degrees
c = 2 * atan2(sqrt(a), sqrt(1 - a))
return c * float(R)
def spherical_cosine_distance(x1, y1, x2, y2):
"""
Calculate the 'as the crow flies' distance between two locations along a sphere using
the Spherical Law of Cosines.
This is a simpler approach than spherical_haversine_distance() that can give good results
on modern computers. However, precision is not generally as good, and this should not
be used if sun-metre accuracy is required.
"""
# convert to radians
a = radians(y1)
b = radians(y2)
C = radians(x2 - x1)
# spherical law of cosines
x = sin(a) * sin(b) + cos(a) * cos(b) * cos(C)
# return as distance
return acos(x) * R
''' CALCULATING DISTANCE ON AN ELLIPSOID'''
def ellipsoidal_inverse_vincenty_distance(x1, y1, x2, y2):
"""
Calculate the 'as the crow flies' distance between two locations along an ellipsoid using
the Inverse Vincenty method.
This version is really just to demonstrate the complexity of this method. I would recommend
the implementation in `ellipsoidal_inverse_vincenty_distance_2` (below) for use in your
own code.
This version is hard-coded to WGS84:
WGS84 = (a=6378137.0, b=6356752.31425, f=298.257223563)
"""
# inverse flattening for ellipsoid
f = 1 / float(298.257223563)
# eccentricity calculations
e2 = e2 = f * (2 - f) # 1st eccentricity squared
e22 = e2 / (1 - e2) # 2nd eccentricity squared
_iterations = 50
try:
EPS = sys.float_info.epsilon
except AttributeError:
EPS = 2.2204460492503131e-16
c1, s1, _ = _r3(y1, f)
c2, s2, _ = _r3(y2, f)
c1c2, s1s2 = c1 * c2, s1 * s2
c1s2, s1c2 = c1 * s2, s1 * c2
ll = dl = radians(abs(x2 - x1))
for _ in range(_iterations):
cll, sll, ll_ = cos(ll), sin(ll), ll
t2 = c2 * sll, c1s2 - s1c2 * cll
ss = hypot(*t2)
if ss < EPS:
print ("whoops!")
return -666
cs = s1s2 + c1c2 * cll
s = atan2(ss, cs)
sa = c1c2 * sll / ss
c2a = 1 - (sa * sa)
if abs(c2a) < EPS:
c2a = 0 # equatorial line
ll = dl + f * sa * s
else:
c2sm = cs - 2 * s1s2 / c2a
ll = dl + _dl(f, c2a, sa, s, cs, ss, c2sm)
if abs(ll - ll_) < 1.0e-12: # about 0.006 mm
break
else:
print ("whoops!")
return -666
if c2a: # e22 == (a / b) ^ 2 - 1
A, B = _p2(c2a, e22)
s = A * (s - _ds(B, cs, ss, c2sm))
b = 6356752.31425 # semi minor axis in m
d = b * s
return d
def _p2(c2a, ab2): # A, B polynomials
u2 = c2a * ab2 # e'2 WGS84 = 0.00673949674227643
A = u2 / 16384.0 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2))) + 1
B = u2 / 1024.0 * ( 256 + u2 * (-128 + u2 * ( 74 - 47 * u2)))
return A, B
def _r3(a, f): # reduced cos, sin, tan
t = (1 - f) * tan(radians(a))
c = 1 / hypot(1, t)
s = t * c
return c, s, t
def _dl(f, c2a, sa, s, cs, ss, c2sm):
C = f / 16.0 * c2a * (4 + f * (4 - 3 * c2a))
return (1 - C) * f * sa * (s + C * ss * (c2sm + C * cs * (c2sm * c2sm * 2 - 1)))
def _ds(B, cs, ss, c2sm):
c2sm2 = 2 * c2sm * c2sm - 1
ss2 = (ss * ss * 4 - 3) * (c2sm2 * 2 - 1)
return B * ss * (c2sm + B / 4.0 * (c2sm2 * cs - B / 6.0 * c2sm * ss2))
def ellipsoidal_inverse_vincenty_distance_2(x1, y1, x2, y2):
"""
Calculate the 'as the crow flies' distance between two locations along an ellipsoid using
the Inverse Vincenty method, using the implementation built in to the PyProj library.
"""
return Geod(ellps='WGS84').inv(x1, y1, x2, y2)[2]
if __name__ == '__main__':
"""
* HERE IS A DEMO TO EXAMINE THE IMPACT OF THER DIFFERENT METHODS ON THE BORDER OF USA AND MEXICO
"""
# imports
from pyproj.crs import CRS
from pyproj.transformer import Transformer
from geopandas import read_file, GeoSeries
# get border between USA and Mexico
world = read_file('./data/natural-earth/ne_50m_admin_0_countries.shp')
border = world[(world.ISO_A3 == 'USA')]['geometry'].iloc[0].intersection(world[(world.ISO_A3 == 'MEX')]['geometry'].iloc[0])
# set up transformers
wgs84 = CRS('+proj=longlat +datum=WGS84 +no_defs')
global_transformer = Transformer.from_crs(wgs84, CRS('+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs'), always_xy=True)
local_transformer = Transformer.from_crs(wgs84, CRS('+proj=lcc +lat_1=20 +lat_2=60 +lat_0=40 +lon_0=-96 +x_0=0 +y_0=0 +datum=NAD83 +units=m +no_defs'), always_xy=True)
# ellipsoidal
local_proj = local_proj2 = global_proj = global_proj2 = equirectangular = cosine = haversine = ellipsoidal = ellipsoidal2 = 0
for line_seg in border.geoms:
# projected
xs, ys = global_transformer.transform([line_seg.coords[0][0],line_seg.coords[1][0]],
[line_seg.coords[0][1], line_seg.coords[1][1]])
global_proj += planar_hypotenuse_distance(xs[0], ys[0], xs[1], ys[1])
global_proj2 += planar_hypotenuse_distance_2(xs[0], ys[0], xs[1], ys[1])
xs, ys = local_transformer.transform([line_seg.coords[0][0],line_seg.coords[1][0]],
[line_seg.coords[0][1], line_seg.coords[1][1]])
local_proj += planar_hypotenuse_distance(xs[0], ys[0], xs[1], ys[1])
local_proj2 += planar_hypotenuse_distance_2(xs[0], ys[0], xs[1], ys[1])
# spherical
equirectangular += spherical_equirectangular_distance(line_seg.coords[0][0],
line_seg.coords[0][1], line_seg.coords[1][0], line_seg.coords[1][1])
cosine += spherical_cosine_distance(line_seg.coords[0][0],
line_seg.coords[0][1], line_seg.coords[1][0], line_seg.coords[1][1])
haversine += spherical_haversine_distance(line_seg.coords[0][0],
line_seg.coords[0][1], line_seg.coords[1][0], line_seg.coords[1][1])
# ellipsoidal
ellipsoidal += ellipsoidal_inverse_vincenty_distance(line_seg.coords[0][0],
line_seg.coords[0][1], line_seg.coords[1][0], line_seg.coords[1][1])
ellipsoidal2 += ellipsoidal_inverse_vincenty_distance_2(line_seg.coords[0][0],
line_seg.coords[0][1], line_seg.coords[1][0], line_seg.coords[1][1])
# convert to km
local_proj /=1000
global_proj /=1000
local_proj2 /=1000
global_proj2 /=1000
equirectangular /=1000
cosine /=1000
haversine /=1000
ellipsoidal /=1000
ellipsoidal2 /=1000
# print output
print ("")
print ("Planar:")
print (f"\tGlobal Projection: \t\t{global_proj:,.2f}km ({global_proj - ellipsoidal2:.2f}km)")
print (f"\tGlobal Projection (hypot): \t{global_proj2:,.2f}km ({global_proj2 - ellipsoidal2:.2f}km)")
print (f"\tLocal Projection : \t\t{local_proj:,.2f}km ({local_proj - ellipsoidal2:.2f}km)")
print (f"\tLocal Projection (hypot): \t{local_proj2:,.2f}km ({local_proj2 - ellipsoidal2:.2f}km)")
print ("Spherical:")
print (f"\tEquirectangular: \t\t{equirectangular:,.2f}km ({equirectangular - ellipsoidal2:.2f}km)")
print (f"\tHaversine: \t\t\t{haversine:,.2f}km ({haversine - ellipsoidal2:.2f}km)")
print (f"\tSpherical Cosine : \t\t{cosine:,.2f}km ({cosine - ellipsoidal2:.2f}km)")
print ("Ellipsoidal:")
print (f"\tInverse Vincenty: \t\t{ellipsoidal:,.2f}km ({ellipsoidal - ellipsoidal2:.2f}km)")
print (f"\tInverse Vincenty (pyproj): \t{ellipsoidal2:,.2f}km (reference distance)")
print ("")
Output:
Planar:
Global Projection: 2,873.54km (394.32km)
Global Projection (hypot): 2,873.54km (394.32km)
Local Projection : 2,370.25km (-108.97km)
Local Projection (hypot): 2,370.25km (-108.97km)
Spherical:
Equirectangular: 2,478.27km (-0.95km)
Haversine: 2,478.27km (-0.95km)
Spherical Cosine : 2,478.27km (-0.95km)
Ellipsoidal:
Inverse Vincenty: 2,479.22km (-0.00km)
Inverse Vincenty (pyproj): 2,479.22km (reference distance)
Some of the code above is based upon the approaches described by Chris Veness here and Jean Brouwers Python port of those approaches, available here. If you want to learn more about geodesy algorithms, there is no better place to start than Chris Veness’s website: