Consuming GeoCode YHOO API via REST - Windows Phone Services Consumption - Part 3

This is the third part of our "Consuming Services with Windows Phone" series. Previously we covered using WebClient and HttpWebRequest to get raw XML data from a URL on Windows Phone.

Now that we know how to fetch data from a URL, let's take a look at how to parse and process the returned data.

First, let's locate a URL that can return some "interesting" data.  Yahoo offers some mapping APIs which are fun to use.  One I've used before is their GeoCode API (  https://developer.yahoo.com/maps/rest/V1/geocode.html ). The geocode API allows you to pass in a combination of street and/or city in order to obtain latitude and logitude points.

The Yahoo GeoCode page offers up a sample URL for us to explore.  https://local.yahooapis.com/MapsService/V1/geocode?appid=YD-9G7bey8_JXxQP6rxl.fBFGgCdNjoDMACQA--&street=701+First+Ave&city=Sunnyvale&state=CA .  If you click on the URL, you will see formatted XML data returned, as shown below. Since we know how to work with URLs via HttpWebRequest and receive down raw data, we must now work with parsing the data into a useful form.

 <?xml version="1.0"?>
<ResultSet xsi:schemaLocation="urn:yahoo:maps https://local.yahooapis.com/MapsService/V1/GeocodeResponse.xsd" 
   xmlns="urn:yahoo:maps" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance">
<!-- ws01.ydn.gq1.yahoo.com uncompressed/chunked Thu May 5 11:23:15 PDT 2011 -->
<Result precision="address">
   <Latitude>37.416397</Latitude>
   <Longitude>-122.025055</Longitude>
   <Address>701 1st Ave</Address>
   <City>Sunnyvale</City>
   <State>CA</State>
   <Zip>94089-1019</Zip>
   <Country>US</Country>
</Result>
</ResultSet>

We are not going to explore setting up a HttpWebRequest and fetching raw results back. If you're unsure how to do that check out the HttpWebRequest article in this series. We start at the point where we have received the results from our HttpWebRequest call. Instead of updating the screen with raw xmlresults, we'll parse and process the results into structured data.

Windows Phone includes excellent support for LINQ to XML. LINQ to XML is a substitute for using XPath and XQuery and other such things for parsing XML data. We shall use LINQ to process the returned results in strong typed GeoCode results.

Using the instructions in the HttpWebRequest article, put together a project that scrapes the URL for a GeoCode result for the Tampa Microsoft Office - https://local.yahooapis.com/MapsService/V1/geocode?appid=YD-9G7bey8_JXxQP6rxl.fBFGgCdNjoDMACQA--&street=5426+Bay+Center+Drive&city=Tampa&state=FL . Run and test your project in the emulator and make sure you are getting the correct results back, as shown in Figure 1.  Note this URL only returns a single result.  We'll move to a GeoCode query that returns multiple results in a bit .

rawXML
Figure1. Raw XML

To structured XML data into a strongly typed class using LINQ to XML, we should setup a class that roughly matches the fields contained in the result set. Add a class file to your project. Name it GeoCodeResult.cs . The class should look as follows:

 public class GeoCodeResult
{
    public string Latitude { get; set; }
    public string Longitude { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    public string Country { get; set; }
}

Next we want to use LINQ to XML to parse our results into a collection of GeoCodeResult. First create an XDocument from the results. The XDocument creates a node representation of the document we can use a LINQ query against.

     System.Xml.Linq.XDocument _xdoc = System.Xml.Linq.XDocument.Parse(results);

When first exploring this particular result set, I thought to myself, "Hey, I know LINQ to XML, I know my class struct, my XDocument is created, this should be cake to parse.". Accordingly, I setup the following code to parse out the first ResultSet from the returned results. Woops. "XML parsing failed" came up the app executed. Can you spot the problem? Neither could I. At a cost of two hours of my life. Hopefully those same two hours you will never lose after learning about XName.

 GeoCodeResult _geocode = 
(from _result in _xdoc.Descendants("Result")
select new GeoCodeResult
{
    Latitude = _result.Element("Latitude").Value,
    Longitude = _result.Element("Longitude").Value,
    Address = _result.Element("Address").Value,
    City = _result.Element("City").Value,
    State = _result.Element("State").Value,
    Zip = _result.Element("Zip").Value,
    Country = _result.Element("Country").Value,
}).FirstOrDefault();

if (_geocode == null)
{
    TextBlockResults.Text = "xml parsing failed";
    return;
}

string _output = string.Format("lat={0}\nlon={1}\naddr={2}", 
    _geocode.Latitude, _geocode.Longitude, _geocode.Address);

TextBlockResults.Text = _output;

Looking at the _xdoc in the debugger offered no clues. So I introduced the following debug code above my LINQ to XML statement, the idea being to dump the doc descendants as they appeared in the XDocument and get a look at what was going on.

 IEnumerable<XElement> _out = 
    from _result in _xdoc.Descendants() select _result;

var _geocode = from _result in _xdoc.Descendants("{urn:yahoo:maps}Result") select _result;

System.Diagnostics.Debug.WriteLine("_out.Descendants.Count()");
foreach (XElement _xe in _out.Descendants())
{
System.Diagnostics.Debug.WriteLine(_xe.Name);
}

The results are shown below. I was surprised to find the namspace "{urn:yahoo:maps}" prefixed to all the field names. Obviously, when querying elements by name, I had a bit of an issue as "Result" is really "{urn:yahoo:maps}Result".

 _out.Descendants.Count()
15
{urn:yahoo:maps}Result
{urn:yahoo:maps}Latitude
{urn:yahoo:maps}Longitude
{urn:yahoo:maps}Address
{urn:yahoo:maps}City
{urn:yahoo:maps}State
{urn:yahoo:maps}Zip
{urn:yahoo:maps}Country
{urn:yahoo:maps}Latitude
{urn:yahoo:maps}Longitude
{urn:yahoo:maps}Address
{urn:yahoo:maps}City
{urn:yahoo:maps}State
{urn:yahoo:maps}Zip
{urn:yahoo:maps}Country

I'd heard about XName, now was the chance to use it. XName is "the proper" way of building namespace plus elements when querying.  I used XName to revise my LINQ query as follows.

 GeoCodeResult _geocode = 
(from _result in _xdoc.Descendants(XName.Get( "Result", "urn:yahoo:maps"))
select new GeoCodeResult
{
    Latitude = _result.Element( XName.Get( "Latitude", "urn:yahoo:maps" )).Value,
    Longitude = _result.Element( XName.Get( "Longitude", "urn:yahoo:maps" )).Value,
    Address = _result.Element( XName.Get( "Address", "urn:yahoo:maps" )).Value,
    City = _result.Element( XName.Get( "City", "urn:yahoo:maps" )).Value,
    State = _result.Element( XName.Get( "State", "urn:yahoo:maps" )).Value,
    Zip = _result.Element( XName.Get( "Zip", "urn:yahoo:maps" )).Value,
    Country = _result.Element( XName.Get( "Country", "urn:yahoo:maps")).Value
}).FirstOrDefault();

Voila. Code worked like a champ, as you can see from in Figure 2 below.

single-result-parsed
Figure2. Parsed Result

Pretty good. Geocoding for a specific street address is working. Below you can find the final code blocks for hitting a single result set.

 public partial class YahooGeoCodeV1Page : PhoneApplicationPage
{
    public string TargetURL { get; set; }
    public YahooGeoCodeV1Page()
    {
        InitializeComponent();
        TargetURL = 
            @"https://local.yahooapis.com/MapsService/V1/geocode" + 
            "?appid=YD-9G7bey8_JXxQP6rxl.fBFGgCdNjoDMACQA--" +
            "&street=5426+Bay+Center+Drive&city=Tampa&state=FL";
        TextBlockTargetUri.Text = TargetURL;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        TextBlockResults.Text = string.Empty;
        TextBlockElapsedTime.Text = "running";

        System.Uri targetUri = new System.Uri(TextBlockTargetUri.Text);
        HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(targetUri);
        request.BeginGetResponse(new AsyncCallback(ReadWebRequestCallback), request); 
    }

    private void ReadWebRequestCallback(IAsyncResult callbackResult)
    {
        try
        {
            HttpWebRequest myRequest = (HttpWebRequest)callbackResult.AsyncState;
            
            HttpWebResponse myResponse = 
                (HttpWebResponse)myRequest.EndGetResponse(callbackResult);

            using (StreamReader httpwebStreamReader = 
                new StreamReader(myResponse.GetResponseStream()))
            {
                string results = httpwebStreamReader.ReadToEnd();
                Dispatcher.BeginInvoke(() => ParseResults( results ));
            }
            myResponse.Close();
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.ToString());
            Dispatcher.BeginInvoke(() => TextBlockResults.Text = ex.ToString());
        }
    }

    private void ParseResults(string results)
    {
        if (string.IsNullOrEmpty(results))
        {
            TextBlockResults.Text = "empty result set returned";
            return;
        }

        System.Xml.Linq.XDocument _xdoc = System.Xml.Linq.XDocument.Parse(results);
        GeoCodeResult _geocode = 
        (from _result in _xdoc.Descendants(XName.Get( "Result", "urn:yahoo:maps"))
        select new GeoCodeResult
        {
            Latitude = _result.Element( XName.Get( "Latitude", "urn:yahoo:maps" )).Value,
            Longitude = _result.Element( XName.Get( "Longitude", "urn:yahoo:maps" )).Value,
            Address = _result.Element( XName.Get( "Address", "urn:yahoo:maps" )).Value,
            City = _result.Element( XName.Get( "City", "urn:yahoo:maps" )).Value,
            State = _result.Element( XName.Get( "State", "urn:yahoo:maps" )).Value,
            Zip = _result.Element( XName.Get( "Zip", "urn:yahoo:maps" )).Value,
            Country = _result.Element( XName.Get( "Country", "urn:yahoo:maps")).Value
        }).FirstOrDefault();

        if (_geocode == null)
        {
            System.Diagnostics.Debug.WriteLine("xml parsing failed");
            TextBlockResults.Text = "xml parsing failed";
            return;
        }

        string _output = 
        string.Format("lat={0}\nlon={1}\naddr={2}", 
            _geocode.Latitude, _geocode.Longitude, _geocode.Address);
        
        TextBlockResults.Text = _output;
    }
}

But what if I just query for the town of "Covington"? Run this query and check it out - https://local.yahooapis.com/MapsService/V1/geocode?appid=YD-9G7bey8_JXxQP6rxl.fBFGgCdNjoDMACQA--&city=Covington . There are multiple Covington's out there. And there will be multiple results returned. How do we deal with multiple results sets being returned? Save the parsing thoughts for the next installment of the series where we'll take a look at dealing with multiple results sets being returned from the XML query, and processing them out on Windows Phone.

YHOO thoughts Why'd I use Yahoo APIs? They've got a some pretty cool API at their GeoCoding site - https://developer.yahoo.com/maps/rest/V1/geocode.html . If I used all Microsoft APIs to test against, well, nice for us, but not so real world. Note this is a deprecated api. You should get your own yahoo key, the one we use here is the sample one on the Yahoo website. Also note calls to this API are limited to 5000 hits per day. Before using in production read the Yahoo licensing info. Details on all that and more are at https://developer.yahoo.com/maps/rest/V1/geocode.html  .

Next up in the series.... Parsing multiple GeoCode result sets from the Yahoo GeoCode API

Consuming Windows Phone Services Series
Part 1 - Web Client Basics - https://blogs.msdn.com/b/devfish/archive/2011/04/06/webclient-windows-phone-services-consumption-part-1.aspx
Part 2 - HttpWebRequest Fundamentals - https://blogs.msdn.com/b/devfish/archive/2011/04/07/httpwebrequest-fundamentals-windows-phone-services-consumption-part-2.aspx
Part 3 - Parsing REST based XML Data - Part A - Single Result - https://blogs.msdn.com/b/devfish/archive/2011/05/05/consuming-geocode-yhoo-api-via-rest-windows-phone-services-consumption-part-3.aspx
Part 4 - Parsing REST based XML Data - Part B - Multiple Results - https://blogs.msdn.com/b/devfish/archive/2011/05/10/consuming-geocode-yhoo-api-via-rest-dealing-with-multiple-results-part-4.aspx