Calculate distance, bearing and more between two Latitude/Longitude points

 

Distance

This script calculates great-circle distances between the two points – that is, the shortest distance over the earth’s surface – using the ‘Haversine’ formula.

It assumes a spherical earth, ignoring ellipsoidal effects – which is accurate enough* for most purposes… – giving an ‘as-the-crow-flies’ distance between the two points (ignoring any hills!).

Enter the co-ordinates into the text boxes to try it out. It accepts a variety of formats:

  • deg-min-sec suffixed with N/S/E/W (e.g. 40°44′55″N, 73 59 11W), or
  • signed decimal degrees without compass direction, where negative indicates west/south (e.g. 40.7486, -73.9864):

Lat 1: Long 1:

Lat 2: Long 2:

And you can see it on a map (thanks to the nice guys at Google Maps)

Haversine formula:

R = earth’s radius (mean radius = 6,371km)
Δlat = lat2− lat1
Δlong = long2− long1
a = sin²(Δlat/2) + cos(lat1).cos(lat2).sin²(Δlong/2)
c = 2.atan2(√a, √(1−a))
d = R.c

(Note that angles need to be in radians to pass to trig functions).

JavaScript:
var R = 6371; // km
var dLat = (lat2-lat1).toRad();
var dLon = (lon2-lon1).toRad(); 
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
        Math.cos(lat1.toRad()) * Math.cos(lat2.toRad()) * 
        Math.sin(dLon/2) * Math.sin(dLon/2); 
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 
var d = R * c;

The Haversine formula ‘remains particularly well-conditioned for numerical computation even at small distances’ – unlike calculations based on the spherical law of cosines. (It was published by R W Sinnott in Sky and Telescope, 1984; the ‘half-versed-sine’ is (1-cosθ)/2, or sin²(θ/2) – don’t ask, I’m not a mathematician).

In fact, when Sinnott devised the Haversine formula, computational precision was limited. Nowadays, JavaScript (and most modern computers) use IEEE 754 64-bit floating-point numbers, which provide 15 significant figures of precision. With this precision, the simple spherical law of cosines formula gives well-conditioned results down to distances as small as around 1 metre. In view of this it is probably worth, in most situations, using either the simpler law of cosines or the more accurate ellipsoidal Vincenty formula in preference to Haversine! (See notes below on the limitations in accuracy of the spherical model).

Spherical law
of cosines:
d = acos(sin(lat1).sin(lat2)+cos(lat1).cos(lat2).cos(long2−long1)).R
JavaScript:
var R = 6371; // km
var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) + 
                  Math.cos(lat1)*Math.cos(lat2) *
                  Math.cos(lon2-lon1)) * R;
Excel:
=ACOS(SIN(Lat1)*SIN(Lat2)+COS(Lat1)*COS(Lat2)*COS(Lon2-Lon1))*6371
(Note that here and in all subsequent code fragments, for simplicity I do not show conversions from degrees to radians; View Source for complete versions).

Bearing

Formula: θ = atan2( sin(Δlong).cos(lat2),
cos(lat1).sin(lat2) − sin(lat1).cos(lat2).cos(Δlong) )
JavaScript:
var y = Math.sin(dLon) * Math.cos(lat2);
var x = Math.cos(lat1)*Math.sin(lat2) -
        Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
var brng = Math.atan2(y, x).toBrng();

Since atan2 returns values in the range -π ... +π (that is, -180° ... +180°), to normalise the result to a compass bearing (in the range 0° ... 360°, with -ve values transformed into the range 180° ... 360°), convert to degrees and then use (θ+360) % 360, where % is modulo.

This is the initial bearing which if followed in a straight line along a great-circle arc (orthodrome) will take you from the start point to the end point; in general, the bearing you are following will have varied by the time you get to the end point (if you were to go from say 35°N,45°E (Baghdad) to 35°N,135°E (Osaka), you would start on a bearing of 60° and end up on a bearing of 120°!).

For final bearing, simply take the initial bearing from the end point to the start point and reverse it (using θ = (θ+180) % 360).

Midpoint

Formula: Bx = cos(lat2).cos(Δlong)
By = cos(lat2).sin(Δlong)
latm = atan2(sin(lat1) + sin(lat2), √((cos(lat1)+Bx)² + By²))
lonm = lon1 + atan2(By, cos(lat1)+Bx)
JavaScript:
var Bx = Math.cos(lat2) * Math.cos(dLon);
var By = Math.cos(lat2) * Math.sin(dLon);
lat3 = Math.atan2(Math.sin(lat1)+Math.sin(lat2),
                  Math.sqrt((Math.cos(lat1)+Bx)*(Math.cos(lat1)+Bx) + 
                             By*By ) ); 
lon3 = lon1.toRad() + Math.atan2(By, Math.cos(lat1) + Bx);

Just as the initial bearing may vary from the final bearing, the midpoint may not be located half-way between latitudes/longitudes; the midpoint between 35°N,45°E and 35°N,135°E is around 45°N,90°E.

 


Destination point given distance and bearing from start point

This page is steadily growing! Given a start point, initial bearing, and distance, this will calculate the destination point and final bearing travelling along a (shortest distance) great circle arc.

Start Lat: Start Long:
Bearing (deg): Distance (km):
Formula: lat2 = asin(sin(lat1)*cos(d/R) + cos(lat1)*sin(d/R)*cos(θ))
  lon2 = lon1 + atan2(sin(brng)*sin(d/R)*cos(lat1), cos(d/R)−sin(lat1)*sin(lat2))
  d/R is the angular distance (in radians), where d is the distance travelled and R is the earth’s radius
JavaScript:
var lat2 = Math.asin( Math.sin(lat1)*Math.cos(d/R) + 
                      Math.cos(lat1)*Math.sin(d/R)*Math.cos(brng) );
var lon2 = lon1 + Math.atan2(Math.sin(brng)*Math.sin(d/R)*Math.cos(lat1), 
                             Math.cos(d/R)-Math.sin(lat1)*Math.sin(lat2));

For final bearing, simply take the initial bearing from the end point to the start point and reverse it (using θ = (θ+180) % 360).

 


Cross-track distance

Here’s a new one: I’ve sometimes been asked about distance of a point from a great-circle path (sometimes called cross track error). I have yet to code & test the following, perhaps someone can confirm it is correct:

Formula: dxt = asin(sin(d13/R)*sin(θ13−θ12))
where d13 is distance from start point to third point
R is the earth’s radius
θ13 is (initial) bearing from start point to third point
θ12 is (initial) bearing from start point to end point

Closest point to the poles

Also new: ‘Clairaut’s formula’ will give you the maximum latitude of a great circle path, given a bearing and latitude on the great circle:

Formula: latmax = acos(abs(sin(θ1)*cos(lat1)))

 


Rhumb lines

A ‘rhumb line’ (or loxodrome) is a path of constant bearing, which crosses all meridians at the same angle.

Sailors used to (still?) navigate along rhumb lines since it is easier to follow a constant compass bearing than to constantly adjust the bearing as is needed to follow a great circle, though they are normally longer than great-circle (orthodrome) routes. Rhumb lines are straight lines on a Mercator Projection map.

Lat 1: Long 1:
Lat 2: Long 2:
Formula: Δφ = ln(tan(lat2/2+π/4)/tan(lat1/2+π/4))
if E:W line q = cos(lat1)
otherwise q = Δlat/Δφ
  d = √[Δlat² + q².Δlon²].R
  θ = atan2(Δlon, Δφ)
  where ln is natural log, Δlon is taking shortest route (<180º), and R is the earth’s radius
JavaScript:
var dPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4));
var q = (Math.abs(dLat) > 1e-10) ? dLat/dPhi : Math.cos(lat1);
// if dLon over 180° take shorter rhumb across 180° meridian:
if (Math.abs(dLon) > Math.PI) {
  dLon = dLon>0 ? -(2*Math.PI-dLon) : (2*Math.PI+dLon);
}
var d = Math.sqrt(dLat*dLat + q*q*dLon*dLon) * R;
var brng = Math.atan2(dLon, dPhi);

Given a start point and a distance d along constant bearing θ, this will calculate the destination point. If you maintain a constant bearing along a rhumb line, you will gradually spiral in towards one of the poles.

Start Lat: Start Long:
Bearing (deg): Distance (km):

 

Formula: α = d/R (angular distance)
  lat2 = lat1 + α.cos(θ)
  Δφ = ln(tan(lat2/2+π/4)/tan(lat1/2+π/4))
if E:W line q = cos(lat1)
otherwise q = Δlat/Δφ
  Δlon = α.sin(θ)/q
  lon2 = (lon1+Δlon+π) % 2.π − π
  where ln is natural log and % is modulo, Δlon is taking shortest route (<180°), and R is the earth’s radius
JavaScript:
lat2 = lat1 + d*Math.cos(brng);
var dPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4));
var q = (Math.abs(lat2-lat1) > 1e-10) ? (lat2-lat1)/dPhi : Math.cos(lat1);
var dLon = d*Math.sin(brng)/q;
// check for some daft bugger going past the pole
if (Math.abs(lat2) > Math.PI/2) lat2 = lat2>0 ? Math.PI-lat2 : -Math.PI-lat2;
lon2 = (lon1+dLon+Math.PI)%(2*Math.PI) - Math.PI;

If you use Ordnance Survey Grid References, I have implemented a script for converting between Lat/Long & OS Grid References.


Convert between degrees-minutes-seconds & decimal degrees

Latitude Longitude 1° ≈ 111 km (110.57 eq’l — 111.70 polar)
1′ ≈ 1.85 km (= 1 nm) 0.001° ≈ 111 m
1″ ≈ 30.9 m 0.00001° ≈ 1 m

No, I’ve not included decimal minutes: a decimal system is easy, a sexagesimal system has merits, but mixing the two is a complete sow’s ear. Switch off the option on your GPS!


Notes:

  • Accuracy: since the earth is not quite a sphere, there are small errors in using spherical geometry; the earth is actually roughly ellipsoidal (or more precisely, oblate spheroidal) with a radius varying between about 6,378km (equatorial) and 6,357km (polar), and local radius of curvature varying from 6,336km (equatorial meridian) to 6,399km (polar). This means that errors from assuming spherical geometry might be up to 0.55% crossing the equator, though generally below 0.3%, depending on latitude and direction of travel. An accuracy of better than 3m in 1km is good enough for me, but if you want greater accuracy, you could use the Vincenty formula for calculating geodesic distances on ellipsoids, which gives results accurate to within 1mm. (Out of sheer perversity – I’ve never needed such accuracy – I looked up this formula and discovered the JavaScript implementation was simpler than I expected).
  • Trig functions take arguments in radians, so latitude, longitude, and bearings in degrees (either decimal or degrees/minutes/seconds) need to be converted to radians, rad = π.deg/180. When converting radians back to degrees (deg = 180.rad/π), West is negative if using signed decimal degrees. For bearings, values in the range -π to +π [-180° to +180°] need to be converted to 0 to +2π [0°–360°]; this can be done by (brng+2.π)%2.π [brng+360)%360] where % is the modulo operator. View page source to see JavaScript functions to handle these conversions.
  • The atan2() function widely used here takes two arguments, atan2(y, x), and computes the arc tangent of the ratio y/x. It is more flexible than atan(y/x), since it handles x=0, and it also returns values in all 4 quadrants -π to +π (the atan function returns values in the range -π/2 to +π/2).
  • If you implement any formula involving atan2 in Microsoft Excel, you will need to reverse the arguments, as Excel has them the opposite way around from JavaScript – conventional order is atan2(y, x), but Excel uses atan2(x, y). To use atan2 in a (VBA) macro, you can use WorksheetFunction.Atan2().
  • All bearings are with respect to true north, 0°=N, 90°=E, etc; if you are working from a compass, magnetic north varies from true north in a complex way around the earth, and the difference has to be compensated for by variances indicated on local maps.
  • I learned a lot from the US Census Bureau GIS FAQ which is no longer available, so I’ve made a copy.
  • Thanks to Ed Williams’ Aviation Formulary for many of the formulae.
  • For miles, divide km by 1.609344
  • For nautical miles, divide km by 1.852
   
 

Principal JavaScript functions for distance calculation are shown below; use ‘View Source’ to see remaining functions for other formulae shown above and how they are used from HTML forms. These functions should be simple to translate into other languages if required.

Update for returning visitors, April 2007: As this page has grown, it’s become somewhat unweildy, so I have now revised and rationalised the scripts to made the code clearer and more re-usable. All LatLon methods now expect and return numeric degrees; converting between radians, degrees & deg/min/sec I have made extensions of Number or String objects. The LatLon object is just a handy way of holding a (latititude,longitude) pair. If you can’t or don’t want to use JavaScript objects, functions could perhaps return a point as an array (e.g. return [lat, lon];).

You are welcome to re-use these scripts [without any warranty express or implied] provided you retain my copyright notice and when possible a link to my website (under a LGPL license). If you have any queries or find any problems, please contact me.

© 2002-2008 Chris Veness

   
/*
 * Use Haversine formula to Calculate distance (in km) between two points specified by 
 * latitude/longitude (in numeric degrees)
 *
 * example usage from form:
 *   result.value = LatLon.distHaversine(lat1.value.parseDeg(), long1.value.parseDeg(), 
 *                                       lat2.value.parseDeg(), long2.value.parseDeg());
 * where lat1, long1, lat2, long2, and result are form fields
 */
LatLon.distHaversine = function(lat1, lon1, lat2, lon2) {
  var R = 6371; // earth's mean radius in km
  var dLat = (lat2-lat1).toRad();
  var dLon = (lon2-lon1).toRad();
  lat1 = lat1.toRad(), lat2 = lat2.toRad();

  var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
          Math.cos(lat1) * Math.cos(lat2) * 
          Math.sin(dLon/2) * Math.sin(dLon/2);
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  var d = R * c;
  return d;
}


/*
 * ditto using Law of Cosines
 */
LatLon.distCosineLaw = function(lat1, lon1, lat2, lon2) {
  var R = 6371; // earth's mean radius in km
  var d = Math.acos(Math.sin(lat1.toRad())*Math.sin(lat2.toRad()) +
                    Math.cos(lat1.toRad())*Math.cos(lat2.toRad())*Math.cos((lon2-lon1).toRad())) * R;
  return d;
}


/*
 * calculate (initial) bearing between two points
 *
 * from: Ed Williams' Aviation Formulary, http://williams.best.vwh.net/avform.htm#Crs
 */
LatLon.bearing = function(lat1, lon1, lat2, lon2) {
  lat1 = lat1.toRad(); lat2 = lat2.toRad();
  var dLon = (lon2-lon1).toRad();

  var y = Math.sin(dLon) * Math.cos(lat2);
  var x = Math.cos(lat1)*Math.sin(lat2) -
          Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
  return Math.atan2(y, x).toBrng();
}


/*
 * calculate destination point given start point, initial bearing (deg) and distance (km)
 *   see http://williams.best.vwh.net/avform.htm#LL
 */
LatLon.prototype.destPoint = function(brng, d) {
  var R = 6371; // earth's mean radius in km
  var lat1 = this.lat.toRad(), lon1 = this.lon.toRad();
  brng = brng.toRad();

  var lat2 = Math.asin( Math.sin(lat1)*Math.cos(d/R) + 
                        Math.cos(lat1)*Math.sin(d/R)*Math.cos(brng) );
  var lon2 = lon1 + Math.atan2(Math.sin(brng)*Math.sin(d/R)*Math.cos(lat1), 
                               Math.cos(d/R)-Math.sin(lat1)*Math.sin(lat2));
  lon2 = (lon2+Math.PI)%(2*Math.PI) - Math.PI;  // normalise to -180...+180

  if (isNaN(lat2) || isNaN(lon2)) return null;
  return new LatLon(lat2.toDeg(), lon2.toDeg());
}


/*
 * construct a LatLon object: arguments in numeric degrees
 *
 * note all LatLong methods expect & return numeric degrees (for lat/long & for bearings)
 */
function LatLon(lat, lon) {
  this.lat = lat;
  this.lon = lon;
}


/*
 * represent point {lat, lon} in standard representation
 */
LatLon.prototype.toString = function() {
  return this.lat.toLat() + ', ' + this.lon.toLon();
}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

// extend String object with method for parsing degrees or lat/long values to numeric degrees
//
// this is very flexible on formats, allowing signed decimal degrees, or deg-min-sec suffixed by 
// compass direction (NSEW). A variety of separators are accepted (eg 3º 37' 09"W) or fixed-width 
// format without separators (eg 0033709W). Seconds and minutes may be omitted. (Minimal validation 
// is done).

String.prototype.parseDeg = function() {
  if (!isNaN(this)) return Number(this);                 // signed decimal degrees without NSEW

  var degLL = this.replace(/^-/,'').replace(/[NSEW]/i,'');  // strip off any sign or compass dir'n
  var dms = degLL.split(/[^0-9.]+/);                     // split out separate d/m/s
  for (var i in dms) if (dms[i]=='') dms.splice(i,1);    // remove empty elements (see note below)
  switch (dms.length) {                                  // convert to decimal degrees...
    case 3:                                              // interpret 3-part result as d/m/s
      var deg = dms[0]/1 + dms[1]/60 + dms[2]/3600; break;
    case 2:                                              // interpret 2-part result as d/m
      var deg = dms[0]/1 + dms[1]/60; break;
    case 1:                                              // decimal or non-separated dddmmss
      if (/[NS]/i.test(this)) degLL = '0' + degLL;       // - normalise N/S to 3-digit degrees
      var deg = dms[0].slice(0,3)/1 + dms[0].slice(3,5)/60 + dms[0].slice(5)/3600; break;
    default: return NaN;
  }
  if (/^-/.test(this) || /[WS]/i.test(this)) deg = -deg; // take '-', west and south as -ve
  return deg;
}
// note: whitespace at start/end will split() into empty elements (except in IE)


/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

// extend Number object with methods for converting degrees/radians

Number.prototype.toRad = function() {  // convert degrees to radians
  return this * Math.PI / 180;
}

Number.prototype.toDeg = function() {  // convert radians to degrees (signed)
  return this * 180 / Math.PI;
}

Number.prototype.toBrng = function() {  // convert radians to degrees (as bearing: 0...360)
  return (this.toDeg()+360) % 360;
}


/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

// extend Number object with methods for presenting bearings & lat/longs

Number.prototype.toDMS = function() {  // convert numeric degrees to deg/min/sec
  var d = Math.abs(this);  // (unsigned result ready for appending compass dir'n)
  d += 1/7200;  // add ½ second for rounding
  var deg = Math.floor(d);
  var min = Math.floor((d-deg)*60);
  var sec = Math.floor((d-deg-min/60)*3600);
  // add leading zeros if required
  if (deg<100) deg = '0' + deg; if (deg<10) deg = '0' + deg;
  if (min<10) min = '0' + min;
  if (sec<10) sec = '0' + sec;
  return deg + '\u00B0' + min + '\u2032' + sec + '\u2033';
}

Number.prototype.toLat = function() {  // convert numeric degrees to deg/min/sec latitude
  return this.toDMS().slice(1) + (this<0 ? 'S' : 'N');  // knock off initial '0' for lat!
}

Number.prototype.toLon = function() {  // convert numeric degrees to deg/min/sec longitude
  return this.toDMS() + (this>0 ? 'E' : 'W');
}

Number.prototype.toPrecision = function(fig) {  // override toPrecision method with one which displays 
  if (this == 0) return 0;                      // trailing zeros in place of exponential notation
  var scale = Math.ceil(Math.log(this)*Math.LOG10E);
  var mult = Math.pow(10, fig-scale);
  return Math.round(this*mult)/mult;
}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */