Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

GPS Receivers, Geodesy, and Geocaching: Vincenty�s Formula

0.00/5 (No votes)
9 Jan 2008 2  
Vincenty's Formula is an iterative solution for calculating the distance and direction between two points along the surface of Earth.

Introduction

Vincenty's Formula is an iterative solution for calculating the distance and direction between two points along the surface of Earth. For clarity, I've stripped out portions of the code I've put up for discussion, but you can download the entire C# source code from the link at the top of this article.

Background

Several years ago, I stumbled on a great pastime called "geocaching[^]." It's a worldwide treasure hunting game where participants use handheld GPS receivers to find hidden "caches" - small boxes filled with prizes, trinkets, and "travel bugs[^]". The caches are hidden by other participants who post nothing more than the latitude and longitude on a website like Geocaching.com[^]. My children and I have had a blast. It's a great way for a grown man to justify playing in the woods (and buying an expensive gadget!) under the pretence of "playing with the kids." With over 420,000 caches in 222 countries on all continents (including Antarctica!) there are bound to be several near you.

Want to know more? Check out this video of my son on a geocache hunt[^].

So, being the true geek I am, I coded up a bunch of applications to record where we've been, where we'd like to go, and interesting waypoints along the way. Often, I'd ask the question "How far is it from here to there?"

The Geodetic Inverse Problem

"How far?" turns out to be a fairly complicated question. For centuries, humankind has known that Earth is round - but everything else about the shape of Earth is less understood. In fact, an entire field called "geodesy[^]", or "geodetics" focuses on understanding the shape of Earth.

If we assume that Earth is a perfect sphere, the math is easy. Given the size of Earth and the latitude and longitude of two points, we can quickly calculate the "great circle[^]" distance and direction from one point to the next.

But, Earth is 'not' a perfect sphere. It's an irregular shape geodesists call the "geoid." Because precise measurements of the geoid and the corresponding distance calculations would be impractical, the geoid is often modeled as an ellipsoid - an object resembling a sphere but slightly flattened at the poles. It's still an inexact model, but it performs far better than the spherical model.

That's where the math gets hard. The Geodetic Inverse Problem - finding the distance and direction from one point to another along an ellipsoid - does not have a closed form solution. However, in 1975, Thaddeus Vincenty[^] developed an extremely accurate iterative solution for which he won the Department of Commerce Medal for Meritorious Service. If you want to sprain your brain, you can read the full publication of his solution[^].

Or, you can just download my C# implementation of Vincenty's Formula[^].

Building the Code

I'll start with a convenient type for encapsulating angles. We'll need this for latitudes and longitudes. It might seem to be an overkill since we could simply uses double instead of a custom Angle type, but humans think in "degrees" while computers do math in "radians." Rather than risk confusion and sprinkle conversion code throughout, I thought this would be easier:

namespace Gavaghan.Geodesy
{
    public struct Angle : IComparable
    {
        private const double PiOver180 = Math.PI / 180.0;
        private double mDegrees;

        static public readonly Angle Zero = new Angle(0);
        static public readonly Angle Angle180 = new Angle(180);

        public Angle( double degrees )
        {
              mDegrees = degrees;
        }

        public double Degrees
        {
            get
            {
                return mDegrees;
            }
            set
            {
                mDegrees = value;
            }
        }

        public double Radians
        {
            get
            {
                return mDegrees * PiOver180;
            }
            set
            {
                mDegrees = value / PiOver180;
            }
        }

        //
        // Equals, IComparable<T> implementation and
        // comparison operators omitted for clarity.
        // See full source code download
        //
    }
}

Some critical portions of the Angle type above are omitted for clarity - like all of the comparison operators. You can check out the complete implementation in the source code download[^].

Next, we have a class that encapsulates a location on an ellipsoid.

namespace Gavaghan.Geodesy
{
    public struct GlobalCoordinates : IComparable<GlobalCoordinates>
    {
        private Angle mLatitude;
        private Angle mLongitude;

        public GlobalCoordinates( Angle latitude, Angle longitude )
        {
            mLatitude = latitude;
            mLongitude = longitude;
        }

        public Angle Latitude
        {
            get
            {
                return mLatitude;
            }
            set
            {
                mLatitude = value;
            }
        }

        public Angle Longitude
        {
            get
            {
                return mLongitude;
            }
                set
            {
                mLongitude = value;
            }
        }
    }
}

Negative latitudes are in the southern hemisphere, and negative longitudes are in the western hemisphere. Once again, the IComparable<T> implementation is omitted for clarity.

Now, we start getting to the fun part. The next type encapsulates the "geodetic curve."

namespace Gavaghan.Geodesy
{
    public struct GeodeticCurve
    {
        private readonly double mEllipsoidalDistance;
        private readonly Angle mAzimuth;
        private readonly Angle mReverseAzimuth;

        public GeodeticCurve( double ellipsoidalDistance,
        Angle azimuth, Angle reverseAzimuth )
        {
            mEllipsoidalDistance = ellipsoidalDistance;
            mAzimuth = azimuth;
            mReverseAzimuth = reverseAzimuth;
        }

        public double EllipsoidalDistance
        {
            get
            {
                return mEllipsoidalDistance;
            }
        }

        public Angle Azimuth
        {
            get
            {
                return mAzimuth;
            }
        }

        public Angle ReverseAzimuth
        {
            get
            {
                return mReverseAzimuth;
            }
        }
    }
}

A "geodetic curve" is the solution we're looking for. It describes how to get from one point on an ellipsoid to another. The ellipsoidal distance is the distance, in meters, between the two points along the surface of the ellipsoid. The azimuth is the direction of travel from the starting point to the ending point. The reverse azimuth, of course, is the direction back from the endpoint (which isn't necessarily a 180 degree turn on an ellipsoid).

The final input we need to Vincenty's Formula is an ellipsoid. We hold onto four parameters of an ellipsoid: the length of each semi-axis (in meters), the flattening ratio, and the inverse of the flattening ratio. Technically, we only need the length of one semi-axis and any one of the other three parameters. We'll record all four for convenience.

namespace Gavaghan.Geodesy
{
    public struct Ellipsoid
    {
        private readonly double mSemiMajorAxis;
        private readonly double mSemiMinorAxis;
        private readonly double mFlattening;
        private readonly double mInverseFlattening;

        private Ellipsoid(double semiMajor, double semiMinor,
                        double flattening, double inverseFlattening)
        {
            mSemiMajorAxis = semiMajor;
            mSemiMinorAxis = semiMinor;
            mFlattening = flattening;
            mInverseFlattening = inverseFlattening;
        }

        static public readonly Ellipsoid WGS84 =
            FromAAndInverseF( 6378137.0, 298.257223563 );
        static public readonly Ellipsoid GRS80 =
            FromAAndInverseF( 6378137.0, 298.257222101 );
        static public readonly Ellipsoid GRS67 =
            FromAAndInverseF( 6378160.0, 298.25 );
        static public readonly Ellipsoid ANS   =
            FromAAndInverseF( 6378160.0, 298.25 );
        static public readonly Ellipsoid Clarke1880 =
            FromAAndInverseF( 6378249.145, 293.465 );

        static public Ellipsoid FromAAndInverseF
            ( double semiMajor, double inverseFlattening )
        {
            double f = 1.0 / inverseFlattening;
            double b = (1.0 - f) * semiMajor;
            return new Ellipsoid(semiMajor, b, f, inverseFlattening);
        }

        public double SemiMajorAxis
        {
            get
            {
                return mSemiMajorAxis;
            }
        }

        public double SemiMinorAxis
        {
            get
            {
            return mSemiMinorAxis;
            }
        }

        public double Flattening
        {
            get
            {
            return mFlattening;
            }
        }

        public double InverseFlattening
        {
            get
            {
                return mInverseFlattening;
            }
        }
    }
}

Generally, you won't need to specify ellipsoid parameters. The Ellipsoid type has a number of static instances that define "reference ellipsoids." Reference ellipsoids represent some organization's consensus on the "best" ellipsoidal parameters to use to model Earth. Two of the most widely accepted reference ellipsoids are defined above: WGS84 (the 1984 World Geodetic System[^]) and GRS80 (the 1980 Geodetic Reference System[^]).

Finally, we have the class that actually implements Vincenty's Formula to solve the Geodetic Inverse Problem given a reference ellipsoid and two sets of global coordinates (equation numbers in comments relate directly to Vincenty's publication[^]):

namespace Gavaghan.Geodesy
{
    public class GeodeticCalculator
    {
        private const double TwoPi = 2.0 * Math.PI;

        public GeodeticCurve CalculateGeodeticCurve( Ellipsoid ellipsoid,
                     GlobalCoordinates start, GlobalCoordinates end )
        {
            // get constants
            double a = ellipsoid.SemiMajorAxis;
            double b = ellipsoid.SemiMinorAxis;
            double f = ellipsoid.Flattening;

            // get parameters as radians
            double phi1 = start.Latitude.Radians;
            double lambda1 = start.Longitude.Radians;
            double phi2 = end.Latitude.Radians;
            double lambda2 = end.Longitude.Radians;

            // calculations
            double a2 = a * a;
            double b2 = b * b;
            double a2b2b2 = (a2 - b2) / b2;

            double omega = lambda2 - lambda1;

            double tanphi1 = Math.Tan(phi1);
            double tanU1 = (1.0 - f) * tanphi1;
            double U1 = Math.Atan(tanU1);
            double sinU1 = Math.Sin(U1);
            double cosU1 = Math.Cos(U1);

            double tanphi2 = Math.Tan(phi2);
            double tanU2 = (1.0 - f) * tanphi2;
            double U2 = Math.Atan(tanU2);
            double sinU2 = Math.Sin(U2);
            double cosU2 = Math.Cos(U2);

            double sinU1sinU2 = sinU1 * sinU2;
            double cosU1sinU2 = cosU1 * sinU2;
            double sinU1cosU2 = sinU1 * cosU2;
            double cosU1cosU2 = cosU1 * cosU2;

            // eq. 13
            double lambda = omega;

            // intermediates we'll need to compute 's'
            double A = 0.0;
            double B = 0.0;
            double sigma = 0.0;
            double deltasigma = 0.0;
            double lambda0;
            bool converged = false;

            for (int i = 0; i < 10; i++)
            {
                lambda0 = lambda;

                double sinlambda = Math.Sin(lambda);
                double coslambda = Math.Cos(lambda);

                // eq. 14
                double sin2sigma = (cosU2 * sinlambda * cosU2 * sinlambda) +
                     (cosU1sinU2 - sinU1cosU2 * coslambda) *
                     (cosU1sinU2 - sinU1cosU2 * coslambda);

                double sinsigma = Math.Sqrt(sin2sigma);

                // eq. 15
                double cossigma = sinU1sinU2 + (cosU1cosU2 * coslambda);

                // eq. 16
                sigma = Math.Atan2(sinsigma, cossigma);

                // eq. 17 Careful! sin2sigma might be almost 0!
                double sinalpha = (sin2sigma == 0) ? 0.0 :
                      cosU1cosU2 * sinlambda / sinsigma;

                double alpha = Math.Asin(sinalpha);
                double cosalpha = Math.Cos(alpha);
                double cos2alpha = cosalpha * cosalpha;

                // eq. 18 Careful! cos2alpha might be almost 0!
                double cos2sigmam = cos2alpha == 0.0 ? 0.0 :
                    cossigma - 2 * sinU1sinU2 / cos2alpha;

                double u2 = cos2alpha * a2b2b2;

                double cos2sigmam2 = cos2sigmam * cos2sigmam;

                // eq. 3
                A = 1.0 + u2 / 16384 * (4096 + u2 *
                    (-768 + u2 * (320 - 175 * u2)));

                // eq. 4
                B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)));

                // eq. 6
                deltasigma = B * sinsigma * (cos2sigmam + B / 4
                    * (cossigma * (-1 + 2 * cos2sigmam2) - B / 6
                    * cos2sigmam * (-3 + 4 * sin2sigma)
                    * (-3 + 4 * cos2sigmam2)));

                // eq. 10
                double C = f / 16 * cos2alpha * (4 + f * (4 - 3 * cos2alpha));

                // eq. 11 (modified)
                lambda = omega + (1 - C) * f * sinalpha
                    * (sigma + C * sinsigma * (cos2sigmam + C
                    * cossigma * (-1 + 2 * cos2sigmam2)));

                // see how much improvement we got
                double change = Math.Abs((lambda - lambda0) / lambda);

                if ((i > 1) && (change < 0.0000000000001))
                {
                     converged = true;
                     break;
                }
            }

            // eq. 19
            double s = b * A * (sigma - deltasigma);
            Angle alpha1;
            Angle alpha2;

            // didn't converge? must be N/S
            if (!converged)
            {
                if (phi1 > phi2)
                {
                    alpha1 = Angle.Angle180;
                    alpha2 = Angle.Zero;
                }
                else if (phi1 < phi2)
                {
                    alpha1 = Angle.Zero;
                    alpha2 = Angle.Angle180;
                }
                else
                {
                    alpha1 = new Angle(Double.NaN);
                    alpha2 = new Angle(Double.NaN);
                }
            }
            // else, it converged, so do the math
            else
            {
                double radians;
                alpha1 = new Angle();
                alpha2 = new Angle();

                // eq. 20
                radians = Math.Atan2(cosU2 * Math.Sin(lambda),
                (cosU1sinU2 - sinU1cosU2 * Math.Cos(lambda)));

                if (radians < 0.0) radians += TwoPi;
                alpha1.Radians = radians;

                // eq. 21
                radians = Math.Atan2(cosU1 * Math.Sin(lambda),
                        (-sinU1cosU2 + cosU1sinU2 *
                Math.Cos(lambda))) + Math.PI;

                if (radians < 0.0) radians += TwoPi;
                alpha2.Radians = radians;
            }

            return new GeodeticCurve(s, alpha1, alpha2);
        }
    }
}

There you have it! You have the tools you need to know the answer to the question "How far is it from here to there?"

Here's an example of using the code to calculate how far it is from the Lincoln Memorial[^] in Washington, D.C. to the Eiffel Tower[^] in Paris:

using System;
using System.Text;
using Gavaghan.Geodesy;

namespace Gavaghan.Geodesy.Example
{
    public class Example
    {
        static void Main()
        {
            // instantiate the calculator
            GeodeticCalculator geoCalc = new GeodeticCalculator();

            // select a reference elllipsoid
            Ellipsoid reference = Ellipsoid.WGS84;

            // set Lincoln Memorial coordinates
            GlobalCoordinates lincolnMemorial;
            lincolnMemorial = new GlobalCoordinates( new Angle(38.88922),
            new Angle(-77.04978) );

            // set Eiffel Tower coordinates
            GlobalCoordinates eiffelTower;
            eiffelTower = new GlobalCoordinates( new Angle(48.85889),
            new Angle(2.29583) );

            // calculate the geodetic curve
            GeodeticCurve geoCurve = geoCalc.CalculateGeodeticCurve(
                    reference, lincolnMemorial, eiffelTower );

            double ellipseKilometers = geoCurve.EllipsoidalDistance / 1000.0;

            Console.WriteLine("Lincoln Memorial to Eiffel Tower using WGS84");
            Console.WriteLine("  Ellipsoidal Distance: {0:0.00} kilometers",
                    ellipseKilometers );
            Console.WriteLine("  Azimuth: {0:0.00} degrees",
                    geoCurve.Azimuth.Degrees );
            Console.WriteLine("  Reverse Azimuth: {0:0.00} degrees",
                    geoCurve.ReverseAzimuth.Degrees );
        }
    }
}

The downloadable library also supports 3-D geodetic calculations (measurements that account for the elevation above or below the reference ellipsoid). For complete source code, documentation, and examples, download the entire C# library[^].

Next Steps

The Vincenty implementation is just one of many useful tools for building a wide variety of applications to support the use of GPS receivers. I think mobile Web apps would be a great choice for GPS users traipsing through the outdoors. I plan to blog about[^] additional tools and ideas I'll be putting together based on my own experiences and reader feedback. Until then, buy your own GPS receiver and go play in the woods!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here