Nieuws
Foto's
Artikelen
Componenten
Applicaties
Kleinkunst

.NET - De Google Maps HTTP geocoding service gebruiken

This article will demonstrate how to use the Google Maps HTTP geocoding service to retrieve the latitude, longitude and a lot of other geographical data from a given address.

 

Introduction

First let me explain something about geographialc data and geocoding. Each location on earth can be described by a coordinate; the latitude (Φ, phi) and the longitude (λ, lambda). Latitude lines are horizontal lines shown running east-to-west on maps. Technically, latitude is an angular measurement in degrees ranging from 0° at the equator to 90° at the poles. The letter N (north) or S (south) is added after the degrees.

Longitude degrees range from 0° to 180° and these lines are drawn from pole to pole. For historical reasons, the meridian passing the Astronomical Observatory in Greenwich, is the one chosen as zero longitude. The longitude degrees are completed with the letter E (east) or W (west).

GPS devices and mapping and geographical software like Google Maps & Google Earth, Microsoft Virtual Earth, Yahoo Maps, Garmin, ... are all based on these coordinates.

Geocoding is the process of assigning geographical information like latitude and longitude to data, e.g. street addresses. A geocoder is a piece of software or a service that enables you to get such geographic information.

I was looking for a way to get the latitude and longitude of a given address within a C# application. Once you have this coordinate, you can display locations in tools like Google Maps. I started with taking a look at the Geocoding API of the Yahoo Maps Web Services. This REST service is quite easy to use but it seems to be limited to addresses in the USA.

After that I did some research about the Google Maps API. This API is mainly a JavaScript client-side solution. Fortunately there is also a HTTP geocoding service. This article will demonstrate how to use this service in a C# .NET application and it will also explain some different techniques to call a REST service.

 

Get latitude and longitude

Google Maps HTTP geocoding service

The Google Maps API Geocoder is a basic REST query, so all the parameters that should be passed to the webservice are included within the query string of the request. The base request URL is http://maps.google.com/maps/geo?. Following parameters are required and have to be included in the string:

  • q : The address that you want to geocode. This is just a string with all information which you have available (street, city, country, ...).
  • key : Your API key. Visit http://code.google.com/apis/maps/signup.html to sign up for a free Google Maps API key.
  • output : The format in which the output should be generated. The options are xml, kml, csv, or (default) json.

Some examples:

http://maps.google.com/maps/geo?q=Kerkstraat+1%2c+3920+Lommel%2c+Belgi%c3%ab&output=xml&key=YOURKEY

http://maps.google.com/maps/geo?q=Tower+Bridge%2c+London&output=csv&key=YOURKEY

http://maps.google.com/maps/geo?q=White+House&output=xml&key=YOURKEY

http://maps.google.com/maps/geo?q=P.O+Box+369+Entebbe+Uganda+&output=csv&key=YOURKEY

http://maps.google.com/maps/geo?q=1600+Amphitheatre+Parkway+Mountain+View+CA+94043+&output=csv&key=YOURKEY

Detailed info about this service can be found at 2 websites

Result as CSV

The easiest way to use this service is to output the result as a CSV file. The result will consist of four numbers, separated by commas:

1. HTTP status code (integer, enumerated type)
2. Accuracy (integer, enumerated type)
3. Latitude (decimal)
4. Longitude (decimal)

Now we know all the basics we can start writing some C# code.

1) First I declared 2 enumerations; GeoStatusCode and GeoAddressAccuracy

using System;
using System.Net;
using System.Xml;
using System.Globalization;
 
namespace ScipBe.Geo.Google
{ 
  // http://code.google.com/apis/maps/documentation/reference.html#GGeoStatusCode
  public enum GeoStatusCode
  {
    Success = 200,
    BadRequest = 400,
    ServerError = 500,
    MissingQuery = 601,
    MissingAddress = 601,
    UnknownAddress = 602,
    UnavailableAddress = 603,
    UnknownDirections = 604,
    BadKey = 610,
    TooManyQueries = 620
  }
 
  // http://code.google.com/apis/maps/documentation/reference.html#GGeoAddressAccuracy
  public enum GeoAddressAccuracy
  {
    UnknownLocation = 0,
    Country = 1,
    Region = 2,
    SubRegion = 3,
    Town = 4,
    PostCode = 5,
    Street = 6,
    Intersection = 7,
    Address = 8,
    Premise = 9
  }

2) Secondly I declared a Coordinate class with float Latitude and Longitude properties and a GeoData class which stores the Status, Accurary and Coordinate.

public class Coordinate
{
  public float Longitude { get; set; }
  public float Latitude { get; set; }
}
 
public class GeoData
{
  public string SearchAddress { get; set; }
  public GeoStatusCode Status { get; set; }
  public GeoAddressAccuracy Accuracy { get; set; }
  public Coordinate Coordinate { get; set; }
}

3) Finally I implemented a GoogleGeocoder class with a static GetGeoData() method which will return a GeoData object. In this method a WebClient (System.Net) object will be instantiated, the URI will be composed and the DownloadString() method will be called.

4) The CSV result will be split into an array and copied into a GeoData object.

I'm using the UriTemplate class, which was introduced in WCF 3.5, to compose the URI. Of course you can accomplish the same by calling the String.Format() or String.Replace() methods. Do not forget to sign up for a Google Maps API key and replace it in my sample code.

  /// <summary>
  /// Google Geocoder class
  /// </summary>
  public class GoogleGeocoder
  {
    private const string googleMapsKey = "FILL IN YOUR OWN KEY";
 
    /// <summary>
    /// Geocoding via HTTP. Pass address to Google Maps server and 
    /// return a GeoData object which contains the coordinates like longitude and latitude
    /// http://code.google.com/apis/maps/documentation/services.html#Geocoding_Direct
    /// </summary>
    /// <param name="address">String with as many address parameters as you want 
    /// (street, housenumber, postcode, city, country, ...)</param>
    /// <returns>GeoData object which contains Status, Accuracy, Longitude and Latitude</returns>
    public static GeoData GetGeoInfo(string address)
    {
      WebClient webClient = new WebClient();
  
      // Fill in arguments of the URI
      Uri baseAddressUri = new Uri("http://maps.google.com/");
      UriTemplate uriTemplate = new UriTemplate("maps/geo?q={address}&output={output}&key={key}");
      Uri formattedUri = uriTemplate.BindByPosition(baseAddressUri, address, "csv", googleMapsKey);
 
      // Call REST service, download CSV and save it as an array
      string[] geoInfoParams = webClient.DownloadString(formattedUri).Split(',');
 
      // Google Maps always returns floats with a dot as decimal separator
      NumberFormatInfo provider = new NumberFormatInfo();
      provider.NumberDecimalSeparator = ".";
 
      /* A reply returned in the CSV format consists of four numbers, separated by commas
       * 1 : HTTP status code
       * 2 : Accuracy 
       * 3 : Latitude 
       * 4 : Longitude
      */
 
      // Create GeoData class and initialize all properties
      return new GeoData()
      {
        SearchAddress = address,
        Status = (GeoStatusCode)Convert.ToInt16(geoInfoParams[0]),
        Accuracy = (GeoAddressAccuracy)Convert.ToInt16(geoInfoParams[1]),
        Coordinate = new Coordinate()
        {
          Latitude = Convert.ToSingle(geoInfoParams[2], provider),
          Longitude = Convert.ToSingle(geoInfoParams[3], provider)
        }
      };
    }
  }
}

 

I've also created a small WPF application to test my GoogleGeocoder class. The following screenshots will show some results:

 

Get detailed geo information

If you change the output parameter from CSV to XML and you take a look at the returned XML file, then you will notice that the XML contains a lot of extra information. Not only the 4 numbers Status, Accurary, Latitude and Longitude are available but also CountryNameCode, AdministrativeAreaName, ThoroughfareName, PremiseName, PostalCodeNumber, ... are included in the XML file. In some cases a collection of corresponding placemarks will be returned.

 

OASIS xAL Standard 2.0

The Google Maps API uses the standard xAL (Address Language) definition to store addresses in XML files. On the OASIS website you can find a lot of information about this standard. The downloadable ZIP file contains a detailed PDF document, XSD & DTD files and several sample XML files.

xAL is a very extensive format with a lot of XML elements. The Google Maps API does not return all information and a lot of the XML elements are not really useful. So I did not create a new class to store a xAL address but I only added some extra properties to my GeoData class.

 

Result as XML

1) I first extended my GeoData class

public class GeoData
{
  public string SearchAddress { get; set; }
  public GeoStatusCode Status { get; set; }
  public GeoAddressAccuracy Accuracy { get; set; }
  public Coordinate Coordinate { get; set; }
 
  public string Id { get; set; }
  public string Address { get; set; }
  public string CountryNameCode { get; set; }
  public string AdministrativeAreaName { get; set; }
  public string SubAdministrativeAreaName { get; set; }
  public string LocalityName { get; set; }
  public string DependentLocalityName { get; set; }
  public string ThoroughfareName { get; set; }
  public string PremiseName { get; set; }
  public string PostalCodeNumber { get; set; }
}

2) Secondly I created a static XDocumentHelper class which can be used in LINQ to XML queries. The GetValue() methods will look for a given element or attribute and return its value.

internal static class XDocumentHelper
{
  public static string GetValue(XElement parentElement, string[] xmlElementPaths, string xmlAttributeName)
  {
    foreach(string path in xmlElementPaths)
    {
      string value = GetValue(parentElement, path, xmlAttributeName);
      if (value.Trim() != "")
        return value;
    }
    return "";
  }
 
  public static string GetValue(XElement parentElement, string xmlElementPath, string xmlAttributeName)
  {
    string[] elementNames = xmlElementPath.Split('/');
    XElement element = parentElement;
 
    foreach (string name in elementNames.Where(n => n != ""))
    {
      element = element.Element(name);
      if (element == null)
      {
        return "";
      }
    }
 
    if (string.IsNullOrEmpty(xmlAttributeName))
    {
      return element.Value;
    }
 
    XAttribute attr = element.Attribute(xmlAttributeName);
    if (attr == null)
    {
      return "";
    }
    return attr.Value;
  }    
}

3) The CSV output parameter of the URI has to be changed to XML. The result will be stored in a string and this will be used to create a XDocument object.

4) Notice that I needed to remove the Google and Oasis XML namespaces before using the KML file in a LINQ to XML query.

5) Make sure to check the Status before parsing the whole XML file.

6) The last step is executing the LINQ to XML query which will return a list of GeoData objects.

    /// <summary>
    /// Geocoding via HTTP. Pass address to Google Maps server and 
    /// return a list of GeoData objects which contains the coordinates and a lot of other geo data
    /// </summary>
    /// <param name="address">String with as many address parameters as you want (street, housenumber, postcode, city, country, ...)</param>
    /// <returns>List of GeoData objects which contains Status, Accuracy, Longitude and Latitude, CountryNameCode,
    /// AdministrativeAreaName, PremiseName, PostalCodeNumber, ...</returns>
    public static IEnumerable<GeoData> GetGeoList(string address)
    {
      // Fill in arguments of the URI
      Uri baseAddressUri = new Uri("http://maps.google.com/");
      UriTemplate uriTemplate = new UriTemplate("maps/geo?q={address}&output={output}&key={key}");
      Uri formattedUri = uriTemplate.BindByPosition(baseAddressUri, address, "xml", googleMapsKey);
 
      // Call REST service and download XML
      WebClient webClient = new WebClient();
      string geoDataXml = webClient.DownloadString(formattedUri);
 
      // Remove the XML namespaces. This is needed to use LINQ to XML
      geoDataXml = geoDataXml.Replace(@"<kml xmlns=""http://earth.google.com/kml/2.0"">", "");
      geoDataXml = geoDataXml.Replace("</kml>", "");
      geoDataXml = geoDataXml.Replace(@"xmlns=""urn:oasis:names:tc:ciq:xsdschema:xAL:2.0""", "");
      XDocument xDoc = XDocument.Parse(geoDataXml);
 
      // Check status of returned data
      GeoStatusCode status = (GeoStatusCode)Convert.ToInt16(XDocumentHelper.GetValue(xDoc.Element("Response"), "Status/code", ""));
 
      // If not successful, then return one simple GeoData object
      if (status != GeoStatusCode.Success)
      {
        return new List<GeoData>() {new GeoData() {SearchAddress = address, Status = status}};
      }
 
      // Google Maps always returns floats with a dot as decimal separator
      NumberFormatInfo provider = new NumberFormatInfo();
      provider.NumberDecimalSeparator = ".";
 
      // LINQ to XML query to parse XML file and return list of placemarks
      // Use the XDocumentHelper class to try to get element and attribute values
      // If not element or attribute is not found, the GetValue() method will return an empty string
      // The XML elements ThoroughfareName, PremiseName and PostalCodeNumber can be stored in 4 different places
      var geoList =
        from item in xDoc.Descendants("Placemark")
        let coordinates = XDocumentHelper.GetValue(item, "Point/coordinates", "")
        select new GeoData()
        {
          SearchAddress = address,
          Status = status,
          Id = XDocumentHelper.GetValue(item, 
            "", "id"),
          Address = XDocumentHelper.GetValue(item, 
            "address", ""),
          Accuracy = (GeoAddressAccuracy)Convert.ToInt16(XDocumentHelper.GetValue(item, 
            "AddressDetails", "Accuracy")),
          Coordinate = new Coordinate()
          {
            Latitude = Convert.ToSingle(coordinates.Split(',')[1], provider),
            Longitude = Convert.ToSingle(coordinates.Split(',')[0], provider)
          },
          CountryNameCode = XDocumentHelper.GetValue(item, 
            "AddressDetails/Country/CountryNameCode", ""),
          AdministrativeAreaName = XDocumentHelper.GetValue(item, 
            "AddressDetails/Country/AdministrativeArea/AdministrativeAreaName", ""),
          SubAdministrativeAreaName = XDocumentHelper.GetValue(item, 
            "AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/SubAdministrativeAreaName", ""),
          LocalityName = XDocumentHelper.GetValue(item,
            new string[] 
              {
                "AddressDetails/Country/AdministrativeArea/Locality/LocalityName",
                "AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/LocalityName"
              }, ""),
          DependentLocalityName = XDocumentHelper.GetValue(item, 
            new string[] 
              {
                "AddressDetails/Country/AdministrativeArea/Locality/DependentLocality/DependentLocalityName",
                "AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/DependentLocality/DependentLocalityName"
              }, ""),
          ThoroughfareName = XDocumentHelper.GetValue(item, 
            new string[] 
              {
                "AddressDetails/Country/AdministrativeArea/Locality/Thoroughfare/ThoroughfareName",
                "AddressDetails/Country/AdministrativeArea/Locality/DependentLocality/Thoroughfare/ThoroughfareName",
                "AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/Thoroughfare/ThoroughfareName",
                "AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/DependentLocality/Thoroughfare/ThoroughfareName"
              }, ""),
          PremiseName = XDocumentHelper.GetValue(item, 
            new string[] 
              {
                "AddressDetails/Country/AdministrativeArea/Locality/Thoroughfare/Premise/PremiseName",
                "AddressDetails/Country/AdministrativeArea/Locality/DependentLocality/Thoroughfare/Premise/PremiseName",
                "AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/Thoroughfare/Thoroughfare/Premise/PremiseName",
                "AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/DependentLocality/Thoroughfare/Premise/PremiseName"
              }, ""),
          PostalCodeNumber = XDocumentHelper.GetValue(item,
            new string[] 
              {
                "AddressDetails/Country/AdministrativeArea/Locality/PostalCode/PostalCodeNumber",
                "AddressDetails/Country/AdministrativeArea/Locality/DependentLocality/PostalCode/PostalCodeNumber",
                "AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/PostalCode/PostalCodeNumber",
                "AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/DependentLocality/PostalCode/PostalCodeNumber"
              }, ""),
        };
 
      return geoList;
    }

Alternative ways to call REST services

WebClient - Asynchronously

The WebClient class also provides a DownloadStringAsync() method which can be used to download data from a URL asynchronously. The benefit of downloading asynchronously is that the UI will not become unresponsive while waiting on the remote server. Just assign a DownloadStringCompletedEventHandler and call the DownloadStringAsync() method. Processing the data should be done in the event handler.

WebClient webClient = new WebClient();
webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(GoogleGeocodingCompleted);
webClient.DownloadStringAsync(formattedUri);
private void GoogleGeocodingCompleted(object sender, DownloadStringCompletedEventArgs e)
{
  if (e.Error == null)
  {
    // Convert array of bytes to string
    Byte[] bytes = e.Result;
    System.Text.ASCIIEncoding encoder = new System.Text.ASCIIEncoding();
    string geoDataXml = encoder.GetString(bytes);
    //...
  }
}

 

HttpWebRequest & HttpWebResponse

Instead of using the WebClient class you could also use the HttpWebRequest and HttpWebResponse classes to get data from a REST service. Once you have a response you can convert this to a string by using a StreamReader.

HttpWebRequest request = HttpWebRequest.Create(formattedUri) as HttpWebRequest;
HttpWebResponse response = request.GetResponse() as HttpWebResponse;
StreamReader reader = new StreamReader(response.GetResponseStream());
string geoDataXml = reader.ReadToEnd();

 

WCF

Another alternative to call REST services is to use the unified programming model offered by WCF. In case of Google Maps I didn't get it working. So following code is posted only for giving an idea how to call REST services with WCF 3.5.

First you have to declare a ServiceContract and decorate the method with the WebGet attribute. Secondly you need to implement a client class which inherits from the generic class ClientBase and your service contract interface

[ServiceContract]
 public interface IGoogleGeocoder
{
  [OperationContract]
  [WebGet(
    ResponseFormat = WebMessageFormat.Xml, 
    UriTemplate = "maps/geo?q={address}&output=xml&key={key}")]
  string GetList(string address, string key);
}
 
 public class GoogleGeocoderClient : ClientBase<IGoogleGeocoder>, IGoogleGeocoder
{
  public string GetList(string address, string key)
  {
    return base.Channel.GetList(HttpUtility.HtmlEncode(address), key);
  }
}

The next step is to set an endpoint in your app.config file. Make sure the use the webHttpBinding and add a webHttp behavior!

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>

    <behaviors>
      <endpointBehaviors>
        <behavior name="webHttpBehavior">
          <webHttp/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <client>
      <endpoint 
        address="http://maps.google.com" 
        behaviorConfiguration="webHttpBehavior"
        binding="webHttpBinding" 
        contract="ScipBe.Geo.IGoogleGeocoder"
        name="GoogleGeocoder" />
    </client>
  </system.serviceModel>
</configuration>
Now you should be able creating a proxy and calling the REST service.
GoogleGeocoderClient proxy = new GoogleGeocoderClient();
string geoDataXml = proxy.GetList(address, googleMapsKey);
((IDisposable)proxy).Dispose();
WCF supports XML and JSON responses out of the box. But Google Maps does return a KML result and that is why this source code will not work. There should be a solution by using a custom binding and implementing a ContentTypeMapperType class to get a response in RAW format, but I didn't get it working.

 

Demo application

I also improved my demo application by adding a Frame control to my WPF Window. This will load a local HTML page which uses the Google Maps JavaScript API. This map will show one of the locations found with the GetGeoList() method. Here are some screenshots. Cool, isn't it ?

 

Coordinate formats

Finally I wanted to improve the Coordinate class. Fortunately I found a nice Coordinate class, developed by Jaime Olivares, on the CodeProject website. This Coordinate class supports several display formats like ISO 6709 and ISO 31-1. Just replace my Coordinate class with the Jaime's one.

public class GeoData
{
  ...
  public ISO_Classes.Coordinate Coordinate { get; set; }
  ...
}
var geoList =

  ...
  select new GeoData()
  {
    ...
    Coordinate = new ISO_Classes.Coordinate()
    {
      ...
    },

Now you can use the ToString() method to display the coordinate as decimals, degrees, ...

I hope that the above examples provide a good overview of the Google Maps HTTP geocoding service and how to handle REST services in general. The full source and a help file of this GoogleGeocoder class can be downloaded in the components section of my website. Have fun with it.