BLOG

C# Distance Between Two Post Codes

08 May 2009 by Stuart Cam

Google provides a fantastic Map API to retrieve address information for a particular post code (zip code to our fellow Americans).

I needed to calculate the distance between two UK post codes, and stumbled across a handy URL for querying Google:

http://maps.google.com/maps/geo?q=SW1A%202AA&output=xml

The URL above retrieves information for 10 Downing Street, London, UK.

Parse the longitude and latitude information from Google for two post codes, in conjuntion with the Haversine Formula for a working solution:

using System;
using System.Net;
using System.Web;
using System.Xml;

namespace Codebrain
{
    public static class Distance
    {
        // Handy structure for Long/Lat information
        public struct Coords
        {
            public double Longitude;
            public double Latitude;
        }

        // Unit calculations
        public enum Units
        {
            Miles,
            Kilometres
        }

        // Will return a null if the Google API is unable to find either post code, or the country constraint fails
        public static double? BetweenTwoPostCodes(string postcodeA, string postcodeB, string countryCodeWithin, Units units)
        {
            var ll1 = PostCodeToLongLat(postcodeA, countryCodeWithin);
            if (!ll1.HasValue) return null;
            var ll2 = PostCodeToLongLat(postcodeB, countryCodeWithin);
            if (!ll2.HasValue) return null;
            return ll1.Value.DistanceTo(ll2.Value, units);
        }

        // Overload for UK post codes
        public static double? BetweenTwoUKPostCodes(string postcodeA, string postcodeB)
        {
            return BetweenTwoPostCodes(postcodeA, postcodeB, "GB", Units.Miles);
        }

        // Uses the Google API to resolve a post code (within the specified country)
        public static Coords? PostCodeToLongLat(string postcode, string countryCodeWithin)
        {
            // Download the XML response from Google
            var client = new WebClient();
            var encodedPostCode = HttpUtility.UrlEncode(postcode);
            var url = string.Format("http://maps.google.com/maps/geo?q={0}&output=xml", encodedPostCode);
            var xml = client.DownloadString(url);
            var doc = new XmlDocument();
            doc.LoadXml(xml);

            // Create a custom namespace manager
            var nsmgr = new XmlNamespaceManager(doc.NameTable);
            nsmgr.AddNamespace("ge", "http://earth.google.com/kml/2.0");
            nsmgr.AddNamespace("oa", "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0");

            // Any results?
            var nodelist = doc.SelectNodes("//ge:kml/ge:Response/ge:Placemark", nsmgr);
            if (nodelist == null || nodelist.Count == 0) return null;

            // Results are already ordered by accuracy, so take the first one
            var node = nodelist[0];

            // Check the Country constraint
            var countryname = node.SelectSingleNode("oa:AddressDetails/oa:Country/oa:CountryNameCode", nsmgr);
            if (countryname.FirstChild.Value != countryCodeWithin)
                return null;

            // Get the raw Long/Lat coordinates (I wish there was a nicer way..
            // perhaps averaging the LongLat enclosing box?)
            var coords = node.SelectSingleNode("ge:Point/ge:coordinates", nsmgr).FirstChild.Value.Split(',');
            double longitude;
            double lattitude;
            if (!Double.TryParse(coords[0], out longitude)) return null;
            if (!Double.TryParse(coords[1], out lattitude)) return null;

            return new Coords
                       {
                           Longitude = longitude,
                           Latitude = lattitude
                       };
        }

        public static double DistanceTo(this Coords from, Coords to, Units units)
        {
            // Haversine Formula...
            var dLat1InRad = from.Latitude * (Math.PI / 180.0);
            var dLong1InRad = from.Longitude * (Math.PI / 180.0);
            var dLat2InRad = to.Latitude * (Math.PI / 180.0);
            var dLong2InRad = to.Longitude * (Math.PI / 180.0);

            var dLongitude = dLong2InRad - dLong1InRad;
            var dLatitude = dLat2InRad - dLat1InRad;

            // Intermediate result a.
            var a = Math.Pow(Math.Sin(dLatitude / 2.0), 2.0) +
                    Math.Cos(dLat1InRad) * Math.Cos(dLat2InRad) *
                    Math.Pow(Math.Sin(dLongitude / 2.0), 2.0);

            // Intermediate result c (great circle distance in Radians).
            var c = 2.0 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1.0 - a));

            // Unit of measurement
            var radius = 6371;
            if (units == Units.Miles) radius = 3959;

            return radius * c;
        }
    }
}

Example Usage:

// The long way...
var distance = Distance.BetweenTwoPostCodes("BS1 6QQ", "SW1A 2AA", "GB", Distance.Units.Miles);
if (distance.HasValue) Console.WriteLine(distance.Value + " miles");
// ...or the short way
var distance = Distance.BetweenTwoUKPostCodes("BS1 6QQ", "SW1A 2AA");
if (distance.HasValue) Console.WriteLine(distance.Value + " miles");

It's not 100% accurate and technically speaking the distance is 'as the crow flies', but if you need simple information it'll do the trick nicely!

Tags: , , ,

Categories: .NET | C Sharp | Web

Comments

11 July 2009 03:53 #

Hello,

I was looking for this and found yours, I haven't tried it yet but I think it will help me.

Regards,

Fawad Lateef

Fawad Lateef

16 April 2010 08:29 #

"var c = 2.0 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1.0 - a));" can be simplified to "var c = 2.0 * Math.Asin(Math.Sqrt(a));"

bob

16 April 2010 09:18 #

@Bob


Thankyou for the improvement!

Stuart Cam

29 April 2010 09:03 #

I found that I had to append the postcode with ,UK  for certain Postcodes to ensure that they ended up in the UK. Other than that, thanks for this Stuart Cam, you've saved me a lot of time with this handy little piece of code, and it's exactly what I needed no more, no less.  Once again thanks!

Martin

01 June 2011 05:47 #

Here is the translation into VB to save anyone else the effort great code much appreciated. Now to figure out how to do the road distance.


Imports Microsoft.VisualBasic
Imports System
Imports System.Xml
Imports System.Web
Imports System.Net

Namespace Post

Public NotInheritable Class Postcode_Distanc

Private Sub New()
End Sub

Public Structure Coords
    Public Longitude As Double
    Public Latitude As Double
End Structure

Public Enum Units
    Miles
    Kilometer
End Enum
' Will return a null if the Google API is unable to find either post code, or the country constraint fails  
Public Shared Function BetweenTwoPostCodes(ByVal postcodeA As String, ByVal postcodeB As String, ByVal countryCodeWithin As String, ByVal units As Units) As System.Nullable(Of Double)
    Dim ll1 = PostCodeToLongLat(postcodeA, countryCodeWithin)
    If Not ll1.HasValue Then
  Return Nothing
    End If
    Dim ll2 = PostCodeToLongLat(postcodeB, countryCodeWithin)
    If Not ll2.HasValue Then
  Return Nothing
    End If
    Return DistanceTo(ll1.Value, ll2.Value, units)
End Function

' Overload for UK post codes  
Public Shared Function BetweenTwoUKPostCodes(ByVal postcodeA As String, ByVal postcodeB As String) As System.Nullable(Of Double)
    Return BetweenTwoPostCodes(postcodeA, postcodeB, "GB", Units.Miles)
End Function

Public Shared Function PostCodeToLongLat(ByVal postcode As String, ByVal countryCodeWithin As String) As System.Nullable(Of Coords)
    ' Download the XML response from Google  
    Dim client = New WebClient()
    Dim encodedPostCode = HttpUtility.UrlEncode(postcode)
    Dim url = String.Format("http://maps.google.com/maps/geo?q={0}&output=xml", encodedPostCode)
    Dim xml = client.DownloadString(url)
    Dim doc = New XmlDocument()
    doc.LoadXml(xml)

    ' Create a custom namespace manager  
    Dim nsmgr = New XmlNamespaceManager(doc.NameTable)
    nsmgr.AddNamespace("ge", "http://earth.google.com/kml/2.0")
    nsmgr.AddNamespace("oa", "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0")

    ' Any results?  
    Dim nodelist = doc.SelectNodes("//ge:kml/ge:Response/geTonglacemark", nsmgr)
    If nodelist Is Nothing OrElse nodelist.Count = 0 Then
  Return Nothing
    End If

    ' Results are already ordered by accuracy, so take the first one  
    Dim node = nodelist(0)

    ' Check the Country constraint  
    Dim countryname = node.SelectSingleNode("oa:AddressDetails/oa:Country/oa:CountryNameCode", nsmgr)
    If countryname.FirstChild.Value <> countryCodeWithin Then
  Return Nothing
    End If

    ' Get the raw Long/Lat coordinates (I wish there was a nicer way..  
    ' perhaps averaging the LongLat enclosing box?)  
    Dim coords = node.SelectSingleNode("geTongoint/ge:coordinates", nsmgr).FirstChild.Value.Split(","c)
    Dim longitude As Double
    Dim lattitude As Double
    If Not [Double].TryParse(coords(0), longitude) Then
  Return Nothing
    End If
    If Not [Double].TryParse(coords(1), lattitude) Then
  Return Nothing
    End If

    Return New Coords() With { _
      .Longitude = longitude, _
      .Latitude = lattitude _
    }
End Function

Public Shared Function DistanceTo(ByVal from As Coords, ByVal [to] As Coords, ByVal units__1 As Units) As Double
    ' Haversine Formula...  
    Dim dLat1InRad = from.Latitude * (Math.PI / 180.0)
    Dim dLong1InRad = from.Longitude * (Math.PI / 180.0)
    Dim dLat2InRad = [to].Latitude * (Math.PI / 180.0)
    Dim dLong2InRad = [to].Longitude * (Math.PI / 180.0)

    Dim dLongitude = dLong2InRad - dLong1InRad
    Dim dLatitude = dLat2InRad - dLat1InRad

    ' Intermediate result a.  
    Dim a = Math.Pow(Math.Sin(dLatitude / 2.0), 2.0) + Math.Cos(dLat1InRad) * Math.Cos(dLat2InRad) * Math.Pow(Math.Sin(dLongitude / 2.0), 2.0)

    ' Intermediate result c (great circle distance in Radians).  
    Dim c = 2.0 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1.0 - a))

    ' Unit of measurement  
    Dim radius = 6371
    If units__1 = Units.Miles Then
  radius = 3959
    End If

    Return radius * c
End Function

End Class

End Namespace

Paul

07 June 2011 04:54 #

@Paul Thanks!

Stuart Cam

Leave A Comment




biuquote
  • Comment
  • Preview
Loading




© Codebrain 2019. All Rights Reserved. Registered in England: 07744920. VAT: GB 119 4078 13