The UK Ordnance Survey is the national mapping agency for Great Britain. Its maps refer to locations using a grid of easting and northing coordinates $(E, N)$ (the British National Grid) instead of a graticule of longitude and latitude, but there is an algorithm for converting between the two, detailed below and in more detail in the OS publication A Guide to Coordinate Systems in Great Britain [PDF]. The topic of geodesy is a complex and interesting one and the interested reader is referred to this document for more information.
A short warning: the following algorithm converted between grid references and longitude/latitude coordinates within the same datum only (for the parameters given here, the OSGB36 on the Airy 1830 ellipsoid, a good local fit to the island of Great Britain). It is not a coordinate transformation, and so cannot be used to transform OS map grid references to other datums. In particular, it will not to convert between grid references and longitude/latitude coordinates within the global WGS84 datum used by most GPS devices).
These parameters apply to the Airy 1830 ellipsoid associated with the OSGB36 datum and the coordinates and conventions of the transverse Mercator projection used on OS maps.
Parameter | Value | Description |
---|---|---|
$a$ | 6377563.396 m | Semi-major axis |
$b$ | 6356256.909 m | Semi-minor axis |
$E_0$ | 400000 m | Map coordinates of true origin |
$N_0$ | -100000 m | |
$\phi_0$ | $49^\circ\,\mathrm{N}$ | Longitude of true origin |
$\lambda_0$ | $2^\circ\,\mathrm{W}$ | Latitude of true origin |
$F_0$ | 0.9996012717 | Scale factor on central meridian |
The square of the ellipsoid's eccentricity constant, $e^2 = \frac{a^2-b^2}{a^2}$.
Define $n = \frac{a-b}{a+b}$.
The following function is used in scaling the latitude coordinate:
\begin{align*} f_M(\phi) = bF_0 & \left[ \left(1 + n + \frac{5}{4}n^2 + \frac{5}{4}n^3\right)(\phi-\phi_0) \right.\\ & - \left( 3n + 3n^2 + \frac{21}{8}n^3\right)\sin(\phi-\phi_0)\cos(\phi+\phi_0)\\ & + \left( \frac{15}{8}n^2 + \frac{15}{8}n^3\right)\sin[2(\phi-\phi_0)]\cos[2(\phi+\phi_0)]\\ & \left. - \frac{35}{24}n^3\sin[3(\phi-\phi_0)]\cos[3(\phi+\phi_0)] \right] \end{align*}
Note: In implementing the algorithm below, ensure that you work in double precision (float
numerical types) and express all angles in radians. Remember that southerly latitudes and westerly longitudes are expressed as negative angles (e.g. $2^\circ\,\mathrm{W}$ is $-2^\circ$).
Given the grid coordinates $(E, N)$ this algorithm returns the latitude and longitude $(\phi, \lambda)$.
First compute $\phi'$ by the following iterative procedure
Set $M=0$, $\phi'=0$.
While $|N-N_0-M| \ge 10^{-5}\,\mathrm{m}$ compute the following
$$ \phi' \rightarrow \phi' + \frac{N-N_0-M}{aF_0}$$
$$M = f_M(\phi')$$
Using this converged value of $\phi'$, calculate the further parameters
\begin{align*} \nu &= \frac{aF_0}{\sqrt{1-e^2\sin^2\phi'}}\\ \rho &= aF_0\frac{1-e^2}{(1-e^2\sin^2\phi')^{3/2}}\\ \eta^2 &= \frac{\nu}{\rho}-1 \end{align*}
Now compute the following coefficients
\begin{align*} c_1 &= \frac{\tan\phi'}{2\rho\nu}\\ c_2 &= \frac{\tan\phi'}{24\rho\nu^3}(5 + 3\tan^2\phi' + \eta^2 - 9\eta^2\tan^2\phi')\\ c_3 &= \frac{\tan\phi'}{720\rho\nu^5}(61 + 90\tan^2\phi' + 45\tan^4\phi')\\ d_1 &= \frac{\sec\phi'}{\nu}\\ d_2 &= \frac{\sec\phi'}{6\nu^3}\left(\frac{\nu}{\rho} + 2\tan^2\phi'\right)\\ d_3 &= \frac{\sec\phi'}{120\nu^5}\left(5 + 28\tan^2\phi' + 24\tan^4\phi'\right)\\ d_4 &= \frac{\sec\phi'}{5040\nu^7}\left(61 + 662\tan^2\phi' + 1320\tan^4\phi' + 720\tan^6\phi'\right) \end{align*}
Finally, calculate the latitude, $\phi$, and longitude, $\lambda$, as:
\begin{align*} \phi &= \phi' - c_1(E-E_0)^2 + c_2(E-E_0)^4 - c_3(E-E_0)^6\\ \lambda &= \lambda_0 + d_1(E-E_0) - d_2(E-E_0)^3 + d_3(E-E_0)^5 - d_4(E-E_0)^7 \end{align*}
Given the latitude and longitude $(\phi, \lambda)$, this algorithm returns the grid coordinates $(E, N)$.
Calculate the parameters
\begin{align*} \nu &= \frac{aF_0}{\sqrt{1-e^2\sin^2\phi}}\\ \rho &= aF_0\frac{1-e^2}{(1-e^2\sin^2\phi)^{3/2}}\\ \eta^2 &= \frac{\nu}{\rho}-1\\ M &= f_M(\phi) \end{align*}
Compute the parameters
\begin{align*} a_1 &= M + N_0\\ a_2 &= \frac{\nu}{2}\sin\phi\cos\phi\\ a_3 &= \frac{\nu}{24}\sin\phi\cos^3\phi(5 - \tan^2\phi + 9\eta^2)\\ a_4 &= \frac{\nu}{720}\sin\phi\cos^5\phi(61 - 58\tan^2\phi + \tan^4\phi)\\ b_1 &= \nu\cos\phi\\ b_2 &= \frac{\nu}{6}\cos^3\phi\left(\frac{\nu}{\rho} - \tan^2\phi\right)\\ b_3 &= \frac{\nu}{120}\cos^5\phi\left(5 - 18\tan^2\phi + \tan^4\phi + 14\eta^2 - 58\eta^2\tan^2\phi\right) \end{align*}
The northing, $N$, and easting, $E$, coordinates are then given by
\begin{align*} N &= a_1 + a_2(\lambda-\lambda_0)^2 + a_3(\lambda-\lambda_0)^4 + a_4(\lambda-\lambda_0)^6\\ E &= E_0 + b_1(\lambda-\lambda_0) + b_2(\lambda-\lambda_0)^3 + b_3(\lambda-\lambda_0)^5 \end{align*}
Write a pair of Python functions, ll_to_os(phi, lam)
and os_to_ll(E, N)
to convert between longitude/latitude and OS grid reference. An example test is given below.
E, N = 544735, 258334 # King's College, Cambridge
print('(E, N) =', (E, N))
phi, lam = os_to_ll(E, N)
print('(φ, λ) = ({:.8f}°, {:.8f}°)'.format(phi, lam))
print(' =', deg_to_dms(phi, 'latitude'), deg_to_dms(lam, 'longitude'))
E, N = ll_to_os(phi, lam)
print('(E, N) =', (E, N))
Output:
(E, N) = (544735, 258334)
(φ, λ) = (52.20380073°, 0.11824087°)
= 52° 12′ 13.6826″ N 0° 7′ 5.6671″ E
(E, N) = (544734.99998566438, 258333.99999784387)
Place the following code in osconv.py
and import the functions into your test script with
from osconv import dms_pretty_print, dms_to_deg, deg_to_dms, os_to_ll, ll_to_os
This module defines some useful functions for pretty-printing angles.
import math
# Ellipsoid parameters for different datums (m): semi-major axis, a, and
# semi-minor axis, b.
datum_ellipsoid = {
# Airy 1830 ellipsoid
'osgb36': {'a': 6.377563396e6,
'b': 6.356256909e6
},
# WGS84 ellipsoid parameters
'wgs84': {'a': 6.378137e6,
'b': 6.3567523141e6
},
}
# Transverse Mercator projection parameters: Map coordinates of true origin,
# (E0, N0), scale factor on central meridian, F0, true origin (phi0, lambda0).
N0 = -100000
E0 = 400000
F0 = 0.9996012717
phi0 = math.radians(49)
lambda0 = math.radians(-2)
def fM(phi, a, b):
"""Return the parameter M for latitude phi using ellipsoid params a, b."""
n = (a-b)/(a+b)
n2 = n**2
n3 = n * n2
dphi, sphi = phi - phi0, phi + phi0
M = b * F0 * (
(1 + n + 5/4 * (n2+n3)) * dphi
- (3*n + 3*n2 + 21/8 * n3) * math.sin(dphi) * math.cos(sphi)
+ (15/8 * (n2 + n3)) * math.sin(2*dphi) * math.cos(2*sphi)
- (35/24 * n3 * math.sin(3*dphi) * math.cos(3*sphi))
)
return M
def dms_pretty_print(d, m ,s, latlong, ndp=4):
"""Return a prettified string for angle d degrees, m minutes, s seconds."""
if latlong=='latitude':
hemi = 'N' if d>=0 else 'S'
elif latlong=='longitude':
hemi = 'E' if d>=0 else 'W'
else:
hemi = '?'
return '{d:d}° {m:d}′ {s:.{ndp:d}f}″ {hemi:1s}'.format(
d=abs(d), m=m, s=s, hemi=hemi, ndp=ndp)
def deg_to_dms(deg, pretty_print_latlong=None, ndp=4):
"""Convert from decimal degrees to degrees, minutes, seconds."""
m, s = divmod(abs(deg)*3600, 60)
d, m = divmod(m, 60)
if deg < 0:
d = -d
d, m = int(d), int(m)
if pretty_print_latlong:
return dms_pretty_print(d, m, s, pretty_print_latlong)
return d, m, s
def dms_to_deg(d, m, s):
"""Convert from degrees, minutes, seconds to decimal degrees."""
return d + m/60 + s/3600
def get_prms(phi, a, F0, e2):
"""Calculate and return the parameters rho, nu, and eta2."""
rho = a * F0 * (1-e2) * (1-e2*math.sin(phi)**2)**-1.5
nu = a * F0 / math.sqrt(1-e2*math.sin(phi)**2)
eta2 = nu/rho - 1
return rho, nu, eta2
def os_to_ll(E, N, datum='osgb36'):
"""Convert from OS grid reference (E, N) to latitude and longitude.
Latitude, phi, and longitude, lambda, are returned in degrees.
"""
a, b = datum_ellipsoid[datum]['a'], datum_ellipsoid[datum]['b']
e2 = (a**2 - b**2)/a**2
M, phip = 0, phi0
while abs(N-N0-M) >= 1.e-5:
phip = (N - N0 - M)/(a*F0) + phip
M = fM(phip, a, b)
rho, nu, eta2 = get_prms(phip, a, F0, e2)
tan_phip = math.tan(phip)
tan_phip2 = tan_phip**2
nu3, nu5 = nu**3, nu**5
sec_phip = 1./math.cos(phip)
c1 = tan_phip/2/rho/nu
c2 = tan_phip/24/rho/nu3 * (5 + 3*tan_phip2 + eta2 * (1 - 9*tan_phip2))
c3 = tan_phip / 720/rho/nu5 * (61 + tan_phip2*(90 + 45 * tan_phip2))
d1 = sec_phip / nu
d2 = sec_phip / 6 / nu3 * (nu/rho + 2*tan_phip2)
d3 = sec_phip / 120 / nu5 * (5 + tan_phip2*(28 + 24*tan_phip2))
d4 = sec_phip / 5040 / nu**7 * (61 + tan_phip2*(662 + tan_phip2*
(1320 + tan_phip2*720)))
EmE0 = E - E0
EmE02 = EmE0**2
phi = phip + EmE0**2 * (-c1 + EmE02*(c2 - c3*EmE02))
lam = lambda0 + EmE0 * (d1 + EmE02*(-d2 + EmE02*(d3 - d4*EmE02)))
return math.degrees(phi), math.degrees(lam)
def ll_to_os(phi, lam, datum='osgb36'):
"""Convert from latitude and longitude to OS grid reference (E, N).
Latitude, phi, and longitude, lambda, are to be provided in degrees.
"""
phi, lam = math.radians(phi), math.radians(lam)
a, b = datum_ellipsoid[datum]['a'], datum_ellipsoid[datum]['b']
e2 = (a**2 - b**2)/a**2
rho, nu, eta2 = get_prms(phi, a, F0, e2)
M = fM(phi, a, b)
sin_phi = math.sin(phi)
cos_phi = math.cos(phi)
cos_phi2 = cos_phi**2
cos_phi3 = cos_phi2 * cos_phi
cos_phi5 = cos_phi3 * cos_phi2
tan_phi2 = math.tan(phi)**2
tan_phi4 = tan_phi2 * tan_phi2
a1 = M + N0
a2 = nu/2 * sin_phi * cos_phi
a3 = nu/24 * sin_phi * cos_phi3 * (5 - tan_phi2 + 9*eta2)
a4 = nu/720 * sin_phi * cos_phi5 * (61 - 58*tan_phi2 + tan_phi4)
b1 = nu * cos_phi
b2 = nu/6 * cos_phi3 * (nu/rho - tan_phi2)
b3 = nu/120 * cos_phi5 * (5 - 18*tan_phi2 + tan_phi4 + eta2*(14 -
58*tan_phi2))
lml0 = lam - lambda0
lml02 = lml0**2
N = a1 + lml02 * (a2 + lml02*(a3 + a4*lml02))
E = E0 + lml0 * (b1 + lml02*(b2 + b3*lml02))
return E, N