Preface

This web page contains worked examples using javascript code. If your browser displays a security warning, e.g. as Internet Explorer in Windows XP will do, you can safely take the action to show the active content. Nothing damaging should happen. If you are unsure, you can view the html source to verify the safety of doing this! The javascript is all standard and makes no reference to anything external to this page.

How to convert latitude and longitude to a British National Grid Reference.

Although there is a lot of involved maths, this is not a difficult task. There are some problems, however, firstly due to the different ways latitude and longitude can be defined. All mapping systems have to use a model of the shape of planet Earth, which is not a perfect sphere. The distance between the poles is less that the distance across the equator, making a shape called an ellipsoid. But the ellipsoid is not a perfect fit either.

Most accurate maps show only part of the surface of the Earth, usually a single country or part of a country. So the usual way of getting round the problem is to define an ellipsoid whose surface has a good alignment with reality over the area the maps is to cover. The Ordnance Survey maps of the United Kingdom use three such reference ellipsoids, one for the Island of Great Britain (England, Scotland and Wales), another for the Island of Ireland, and finally a third ellipsoid to deal with the Channel Islands. This article primarily describes Great Britain, but the same maths can be applied to most of the other two areas.

The traditional ellipsoid adopted for Great Britain is called the Airy Spheroid. This was defined in 1830 by Britain's Astronomer Royal, Sir George Airy. This has a semi-major axis of 6377563.396 metres, and a semi-minor axis of 6356256.910 metres. In the equations that follow we use a constant called the eccentricity, this is simply the diffence between the squares of the axes, divided by the square of the major axis.


  function ecc(major, minor)
  {
	return (major*major - minor*minor) / (major*major);
  }

Using the formula above with the Airy axes we get an eccentricity of

Let us assume that that our measurements of latitude and longitude are based on the Airy spheroid. This may not be true, but I'll cover that later on. Firstly we need to convert our measurements from degrees to radians. There is accomplished by multiplying our latitude or longitude by PI / 180. I shall call this conversion factor deg2rad. A few other constants are also defined:


  function LLtoNE(lat, lon)
  {
    var deg2rad = Math.PI / 180;
    var rad2deg = 180.0 / Math.PI;
    var phi = lat * deg2rad;      // convert latitude to radians
    var lam = lon * deg2rad;   // convert longitude to radians
    a = 6377563.396;       // OSGB semi-major axis
    b = 6356256.91;        // OSGB semi-minor axis
    e0 = 400000;           // OSGB easting of false origin
    n0 = -100000;          // OSGB northing of false origin
    f0 = 0.9996012717;     // OSGB scale factor on central meridian
    e2 = 0.0066705397616;  // OSGB eccentricity squared
    lam0 = -0.034906585039886591;  // OSGB false east
    phi0 = 0.85521133347722145;    // OSGB false north
    var af0 = a * f0;
    var bf0 = b * f0;
    // easting
    var slat2 = Math.sin(phi) * Math.sin(phi);
    var nu = af0 / (Math.sqrt(1 - (e2 * (slat2))));
    var rho = (nu * (1 - e2)) / (1 - (e2 * slat2));
    var eta2 = (nu / rho) - 1;
    var p = lam - lam0;
    var IV = nu * Math.cos(phi);
    var clat3 = Math.pow(Math.cos(phi),3);
    var tlat2 = Math.tan(phi) * Math.tan(phi);
    var V = (nu / 6) * clat3 * ((nu / rho) - tlat2);
    var clat5 = Math.pow(Math.cos(phi), 5);
    var tlat4 = Math.pow(Math.tan(phi), 4);
    var VI = (nu / 120) * clat5 * ((5 - (18 * tlat2)) + tlat4 + (14 * eta2) - (58 * tlat2 * eta2));
    east = e0 + (p * IV) + (Math.pow(p, 3) * V) + (Math.pow(p, 5) * VI);
    // northing
    var n = (af0 - bf0) / (af0 + bf0);
    var M = Marc(bf0, n, phi0, phi);
    var I = M + (n0);
    var II = (nu / 2) * Math.sin(phi) * Math.cos(phi);
    var III = ((nu / 24) * Math.sin(phi) * Math.pow(Math.cos(phi), 3)) * (5 - Math.pow(Math.tan(phi), 2) + (9 * eta2));
    var IIIA = ((nu / 720) * Math.sin(phi) * clat5) * (61 - (58 * tlat2) + tlat4);
    north = I + ((p * p) * II) + (Math.pow(p, 4) * III) + (Math.pow(p, 6) * IIIA);
    east = Math.round(east);       // round to whole number
    north = Math.round(north);     // round to whole number
    nstr = String(north);      // convert to string
    estr = String(east);       // ditto
  }

  function Marc(bf0, n, phi0, phi)
  {
    var Marc = bf0 * (((1 + n + ((5 / 4) * (n * n)) + ((5 / 4) * (n * n * n))) * (phi - phi0))
     - (((3 * n) + (3 * (n * n)) + ((21 / 8) * (n * n * n))) * (Math.sin(phi - phi0)) * (Math.cos(phi + phi0)))
     + ((((15 / 8) * (n * n)) + ((15 / 8) * (n * n * n))) * (Math.sin(2 * (phi - phi0))) * (Math.cos(2 * (phi + phi0))))
     - (((35 / 24) * (n * n * n)) * (Math.sin(3 * (phi - phi0))) * (Math.cos(3 * (phi + phi0)))));
    return(Marc);
  }

This is quite involved maths, and I am not going to try to explain it! This is the same as that used in the Ordnance Survey document A Guide to Coordinate Systems in Great Britain and converts latitude and longitude to a Transverse Mercator projection. These projections are often used for paper (flat) maps. You can find some explanation in the OS document.

The Javascript maths functions are fairly obvious, I hope! You will have to scroll far to the right to see some of the code. Some of the expressions are quite long-winded, so I have split them up a little to avoid too many confusing brackets and Math functions.

We can now try this with some test figures, latitude 52° 39’ 27.2531" north, 1° 43’ 4.5177" east. These figures are taken from a worked example in the Ordnance Survey document.


    lat = 52.65757;  // decimal version of latitude
    lon = 1.71791;   // decimal version of longitude
    LLtoNE(lat, lon);
    document.write("And the results are:");
    document.write("Northings: ",nstr, " metres.");
    document.write("Eastings: ", estr, " metres.");

The NGR northings and eastings are shown along the axes of any Ordnance Survey map. Normally the NGR is shown as letters and numbers. The letters replace the 100km digit(s) of the eastings and northings. This could easily be determined from a simple look-up table, which would need to handle the ninety one 100km 'squares'. So here is a weird formula, source unknown, which does the job in less space:


  function NE2NGR(east, north)
  {
    var eX = east / 500000;
    var nX = north / 500000;
    var tmp = Math.floor(eX)-5.0 * Math.floor(nX)+17.0;
    nX = 5 * (nX - Math.floor(nX));
    eX = 20 - 5.0 * Math.floor(nX) + Math.floor(5.0 * (eX - Math.floor(eX)));
    if (eX > 7.5)
      eX = eX + 1;
    if (tmp > 7.5)
      tmp = tmp + 1;
    var eing = String(east);
    var ning = String(north);
    var lnth = eing.length;
    eing = eing.substring(lnth - 5, lnth);
    lnth = ning.length;
    ning = ning.substring(lnth - 5, lnth);
    ngr = String.fromCharCode(tmp + 65) + String.fromCharCode(eX + 65) + " " + eing + " " + ning;
    return ngr;
  }

So for a worked example using the same latitude and longitude, we run the code:


    lat = 52.65757;
    lon = 1.71791;
    LLtoNE(lat, lon);
    document.write("And the results are:");
    document.write("Northings: ",nstr, " metres.");
    document.write("Eastings: ", estr, " metres.");
    ngr = NE2NGR(east, north);
    document.write("NGR:", ngr);

This is a one metre grid reference, it can be reduced to the standard 100m reference by omitting the last two figures of each five figure group: TG 514 131.

That is basically how it is done. There are a few short cuts, for example I have not taken into account the fact that the NGR grid has a scale factor that varies as you move laterally across the map. You can find appropriate code to deal with this issue in the OS document linked to earlier. I haven't bothered here, in any case the error involved is quite small.

Modifications for Ireland

The OS Irish Grid uses a different reference ellipsoid, the 'Modified Airy'. This has a major axis of 6377340.189 metres, and a minor axis of 6356034.447 metres. The NGR origin is obviously different, and the offsets are also different. So in the constants section we need to alter the code as below:


    a = 6377340.189;      // OSI semi-major
    b = 6356034.447;      // OSI semi-minor
    e0 = 200000;          // OSI easting of false origin
    n0 = 250000;          // OSI northing of false origin
    f0 = 1.000035;        // OSI scale factor on central meridian
    e2 = 0.00667054015;   // OSI eccentricity squared
    lam0 = -0.13962634015954636615389526147909;   // OSI false east
    phi0 = 0.93375114981696632365417456114141;    // OSI false north

The rest of that procedure LL2NE is the same. There is one simple change to the procedure NE2NGR. The first letter is omitted, thus:


    ngr = String.fromCharCode(eX + 65) + " " + eing + " " + ning;

Modifications for the Channel Islands

The Channel Islands are slightly different, and use the International 1924 ellipsoid. The major axis is 6378388 metres, the minor is 6356752.3141 metres. Thus the constants are modified:


    a = 6378388.000;       // INT24 ED50 semi-major
    b = 6356911.946;       // INT24 ED50 semi-minor 
    e0 = 500000;           // CI easting of false origin
    n0 = 0;                // CI northing of false origin
    f0 = 0.9996;           // INT24 ED50 scale factor on central meridian
    e2 = 0.0067226700223333;  // INT24 ED50 eccentricity squared
    lam0 = -0.0523598775598;  // CI false east
    phi0 = 0 * deg2rad;       // CI false north 

The rest of that procedure LL2NE is the same. The letters of the resultant NGR are calculated in a different way. A new procedure to replace NE2NGR is required:


  function NE2CINGR(east, north)
  {
    if (north > 5500000)
    {
      var ngr = "WA " + estr.substring(1, 6) + " " + nstr.substring(2, 7);
    }
    else if (north < 5500000)
    {
      var ngr = "WV " + estr.substring(1, 6) + " " + nstr.substring(2, 7);
    }
    return ngr;
  }

Here is a simple demonstation using northings of 5567890 and eastings of 542345:


    north = 5567890;
    east = 542345;
    ngr = NE2CINGR(east, north);
    document.write("NGR is: ", ngr);

Coping with other ellipsoids

As I described earlier, map makers use an ellipsoid whose surface closely matches the surface of the area to be mapped. In order to define how this ellipsoid and the real Earth are aligned, they use a concept called a Datum (or Terrestrial Reference Frame, TRF). This Datum ties actual points on the surface of the Earth to latitude and longitude points on the reference ellipsoid. Since the Earth and Datum do not match exactly, this tie up distorts the maths a little, and makes it almost impossible to convert one system to another. Conversions are worthwhile, as they may improve accuracy by one hundred metres or more, but they are not exact.

Firstly a table showing some ellipsoids and Datums in use in the UK.

EllipsoidDatumDescription
AiryOSGB36Used for the OS British National Grid
Modified AiryIreland 65Used for the OS Irish National Grid
International 1924ED50Used for the Channel Islands
GRS80WGS84A system used to describe the whole planet, closely related to the GPS system

There are many ways to convert data from one system to another, the most accurate being the most complex! For this example I shall use a 7 parameter Helmert transformation.

The process is in three parts: (1) Convert latitude and longitude to Cartesian coordinates (these also include height data, and have three parameters, X, Y and Z). (2) Transform to the new system by applying the 7 parameters and using a little maths. (3) Convert back to latitude and longitude.

For the example we shall transform a GRS80 location to Airy, e.g. a GPS reading to the Airy spheroid.

The following code converts latitude and longitude to Cartesian coordinates. The input parameters are: WGS84 latitude and longitude, axis is the GRS80/WGS84 major axis in metres, ecc is the eccentricity, and height is the height above the ellipsoid.


   v = axis / (Math.sqrt (1 - ecc * (Math.pow (Math.sin(lat), 2))));
   x = (v + height) * Math.cos(lat) * Math.cos(lon);
   y = (v + height) * Math.cos(lat) * Math.sin(lon);
   z = ((1 - ecc) * v + height) * Math.sin(lat);

The transformation requires the 7 parameters: xp, yp and zp correct the coordinate origin, xr, yr and zr correct the orientation of the axes, and sf deals with the changing scale factors.


  sf = s * 0.000001;  // scale factor is in parts per million
  xrot = (xr / 3600) * deg2rad;
  yrot = (yr / 3600) * deg2rad;
  zrot = (zr / 3600) * deg2rad;
  
  hx = x + (x * sf) - (y * zrot) + (z * yrot) + xp;
  hy = (x * zrot) + y + (y * sf) - (z * xrot) + yp;
  hz = (-1 * x * yrot) + (y * xrot) + z + (z * sf) + zp;

Then we simply convert back to latitude and longitude, where axis and ecc are those of the Airy spheroid:


  var lon = Math.atan(y / x);
  var p = Math.sqrt((x * x) + (y * y));
  var lat = Math.atan(z / (p * (1 - ecc)));
  var v = axis / (Math.sqrt(1 - ecc * (Math.sin(lat) * Math.sin(lat))));
  var errvalue = 1.0;
  var lat0 = 0;
  while (errvalue > 0.001)
  {
    lat0 = Math.atan((z + ecc * v * Math.sin(lat)) / p);  
    errvalue = Math.abs(lat0 - lat);
    lat=lat0;
  }
  var height = p / Math.cos(lat) - v;

The above involves some iteration, hence the while loop. In this case, ecc is the eccentricity of the Airy spheroid, and axis is its major axis. For convenience, I have joined these three bits of code together into one function:


  function transform(lat, lon, a, e, h, a2, e2, xp, yp, zp, xr, yr, zr, s)
  {
    // convert to cartesian; lat, lon are radians
    sf = s * 0.000001;
    v = a / (Math.sqrt(1 - (e *(Math.sin(lat) * Math.sin(lat)))));
    x = (v + h) * Math.cos(lat) * Math.cos(lon);
    y = (v + h) * Math.cos(lat) * Math.sin(lon);
    z = ((1 - e) * v + h) * Math.sin(lat);
    // transform cartesian
    xrot = (xr / 3600) * deg2rad;
    yrot = (yr / 3600) * deg2rad;
    zrot = (zr / 3600) * deg2rad;
    hx = x + (x * sf) - (y * zrot) + (z * yrot) + xp;
    hy = (x * zrot) + y + (y * sf) - (z * xrot) + yp;
    hz = (-1 * x * yrot) + (y * xrot) + z + (z * sf) + zp;
    // Convert back to lat, lon
    lon = Math.atan(hy / hx);
    p = Math.sqrt((hx * hx) + (hy * hy));
    lat = Math.atan(hz / (p * (1 - e2)));
    v = a2 / (Math.sqrt(1 - e2 * (Math.sin(lat) * Math.sin(lat))));
    errvalue = 1.0;
    lat0 = 0;
    while (errvalue > 0.001)
    {
      lat0 = Math.atan((hz + e2 * v * Math.sin(lat)) / p);  
      errvalue = Math.abs(lat0 - lat);
      lat = lat0;
    }
    h = p / Math.cos(lat) - v;
    var geo = { latitude: lat, longitude: lon, height: h };  // object to hold lat and lon
    return(geo);
  }    

Which can be called like this:


  WGS84_AXIS = 6378137;
  WGS84_ECCENTRIC = 0.00669438037928458;
  OSGB_AXIS = 6377563.396;
  OSGB_ECCENTRIC = 0.0066705397616;
  lat = 51.500;  // 51.5 degrees north
  lon = -0.500;  // 0.5 degrees west (negative for west)
  phip = lat * deg2rad;
  lambdap = lon * deg2rad;
  
  var geo = transform(phip, lambdap, WGS84_AXIS, WGS84_ECCENTRIC, height, OSGB_AXIS, OSGB_ECCENTRIC, 
   -446.448, 125.157, -542.06, -0.1502, -0.247, -0.8421, 20.4894);
   
  lat = geo.latitude * rad2deg;
  lon = geo.longitude * rad2deg;

And now we run the code:

To reverse the transformation process, i.e. to convert OSGB36 to WGS84 you simply swap round the positions of the OSGB and WGS84 axis and eccentricity, and reverse the sign of the seven transformation parameters, like this:


  var geo = transform(phip, lambdap, OSGB_AXIS, OSGB_ECCENTRIC, height, WGS84_AXIS, WGS84_ECCENTRIC, 
   446.448, -125.157, 542.06, 0.1502, 0.247, 0.8421, -20.4894);

  return(geo);

And now we run the code using the result from the last time:

Note the inaccuracy is pretty small, I've displayed the full Javascript output although in practice the error represents a few millimetres. Note also that the transform function also uses the height output parameter in calculating the results above. The height used in this function is the height above the datum. OSGB36 has no concept of height, a different datum based on the average sea level at Newlyn is used to display height on OS maps.

Parameters for transformation from the Irish and the Channel Island grids to WGS84 are shown below. The constants for the semi-major axis and eccentricity can be found further up this page.


   var geo = transform(phip, lambdap, IRISH_AXIS, IRISH_ECCENTRIC, height, WGS84_AXIS, WGS84_ECCENTRIC, 
     482.53, -130.596, 564.557, -1.042, -0.214, -0.631, -8.15);


   var geo = transform(phip, lambdap, INT24_AXIS, INT24_ECCENTRIC, height,  WGS84_AXIS, WGS84_ECCENTRIC,
    -83.901, -98.127, -118.635, 0, 0, 0, 0);

Well, that's about it. Most of the work on this page is carried out by functions in the head section (and also in the body) on this html page. They are slightly different from the functions shown in yellow for presentational purposes, but the maths is the same. All results from this code are shown in green. Note that most of the code is lifted from working calculators elsewhere on this site, hence the use of 'var' may not be consistent! There may be too many brackets here and there, as this code has been converted from other programming languages two or three times!

If you need any more information, the Ordnance Survey document mentioned previously will probably help you. Information on the OS Irish Grid can be found at www.osi.ie/en/alist/geodetic-publications.aspx. Information on the Channel Islands is harder to come by. Some of my information has come from an OS map of Jersey, and the rest from hours spent searching the net.

For any more help, please get in touch. My email address can be found elsewhere on this site.

Roger Muggleton. October 2005
Updated July 2008, August/September 2008, June 2011.