Introduction

Beginning where the article Virtual Earth, SQL Server 2008 and AJAX left off, we’ll add to our real estate application by enabling the insertion of new properties into our SQL Server 2008 database by querying the Yahoo Geocode service to retrieve the longitude and latitude of a specified address.

Getting Started with Yahoo Geocoding

The Yahoo Geocoding service can queried using a basic REST query, that is, all parameters that should be passed to the web service are included within the query string of the request. The base request URL is shown below.

http://local.yahooapis.com/MapsService/V1/geocode

However, before we can begin using the web service we must register for a Yahoo application ID, for more information refer to Yahoo's application ID page. The REST web service supports a number of different query string parameters, however in our real estate application we're only going to make use of the required appid parameter along with the free-form text location, that is, the full address contained within a single string. These two parameters are shown in the table below.

Parameter Description
appid The Yahoo application ID. Refer to Yahoo to retrieve your application ID.
location

This parameter will allow us to enter the following information in a single field.

  • city, state
  • city, state, zip
  • zip
  • street, city, state
  • street, city, state, zip
  • street, zip

Now that we've got that our of the way and we've obtained our Yahoo application ID, let's go ahead and examine the REST request URL, which queries Yahoo for the latitude and longitude for the city of Las Vegas, Nevada. The REST request URL is shown below (note that the application ID shown below is not that of an actual application ID).

http://local.yahooapis.com/MapsService/V1/geocode?appid=YahooAppID&location=las%20vegas,%20nevada

Replace the string YahooAppID with your Yahoo application ID and paste the REST request URL into your web browser, the result will be that of the XML shown below.

<?xml version="1.0" ?>
<ResultSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:yahoo:maps" xsi:schemaLocation="urn:yahoo:maps http://api.local.yahoo.com/MapsService/V1/GeocodeResponse.xsd">
  <Result precision="zip">
    <Latitude>36.171860</Latitude>
    <Longitude>-115.139717</Longitude>
    <Address />
    <City>Las Vegas</City>
    <State>NV</State>
    <Zip />
    <Country>US</Country>
  </Result>
</ResultSet>

As you can see, a simple XML response that can be parsed in any number of ways, also note the specified namespace. As far as using the Yahoo Geocode service, there isn't much more to it, the general idea is, make a REST request, retrieve the XML response and parse out the desired data.

Building the Yahoo Geocoding Web Service Method

Recall that in the previous article, Virtual Earth, SQL Server 2008 and AJAX we built a simple ASP.NET web service. In this article we'll implement a new web method. This method will take on the responsibility of constructing the REST request and parsing the response. First and foremost, we'll make use of a System.Net.WebRequest object to create the REST request and we'll utilize a WebResponse object to hold the response of the request, that is, the XML response shown above. Finally, the response will be parsed using some basic LINQ code. The GeocodeAddress() method is shown below.

[WebMethod]
public Pushpins GeocodeAddress(string location)
{
    try
    {
        WebRequest request = WebRequest.Create(BuildYahooUrl(location));
        WebResponse response = request.GetResponse();

        // Parse the response
        XmlTextReader reader = new XmlTextReader(response.GetResponseStream());

        XNamespace xmlns = XNamespace.Get("urn:yahoo:maps");
        var loc = from e in XDocument.Load(reader).Elements(xmlns + "ResultSet").Elements(xmlns + "Result")
                  select new Pushpins
                  {
                      Latitude = e.Element(xmlns + "Latitude").Value,
                      Longitude = e.Element(xmlns + "Longitude").Value,
                      Name = location,
                      Description = "Address: " + e.Element(xmlns + "Address").Value +
                        "<br/>City: " + e.Element(xmlns + "City").Value +
                        "<br/>State: " + e.Element(xmlns + "State").Value
                  };

        return loc.First();
    }
    catch
    {
        return null;
    }
}

Note that an XNamespace object must be defined and used when retrieving elements of the response. Finally, a collection of Pushpins objects is created and the first object of the collection is returned to the caller, that is, our client-side AJAX code. Notice that some general information returned from Yahoo is also included within the Pushpins object, that is, the city, state and address returned from Yahoo is included and will be added to a Virtual Earth map for the user to verify that the returned location is correct. Once the Pushpins object is returned to the client, we'll parse the object properties and confirm that the location is in fact correct prior to persisting the results in the SQL Server 2008 database.

Note the use of the BuildYahooUrl() method in the above listing, this method simply retrieves the Yahoo application ID and base path from the web.config file's <appSettings> element and constructs the REST request URL. The method is defined as follows:

private static string BuildYahooUrl(string location)
{
    StringBuilder sb = new StringBuilder();
    sb.Append(ConfigurationManager.AppSettings["YahooGeocodeUrl"]);

    sb.AppendFormat("?appid={0}&location={1}",
        ConfigurationManager.AppSettings["YahooID"],
        HttpContext.Current.Server.UrlEncode(location));

    return sb.ToString();
}

That's all there is to it! We can invoke this web service method from client-side AJAX code to retrieve the latitude and longitude for a specified address.

Saving the New Home for Sale

We've got the web service method created which can be invoked to query the Yahoo Geocode service to retrieve the latitude and longitude of a specified address, however, we must also build a method that can be invoked to persist the data. Recall that in the previous article, we made use of the new geography data type implemented in SQL Server 2008. Also recall that we previously created a stored procedure that can be invoked to append geography data into our database, for completeness, this stored procedure is defined as follows:

CREATE PROCEDURE [dbo].[VE_InsertProperty]
    @Name VARCHAR(128),
    @Price DECIMAL(7,0),
    @Description VARCHAR(MAX),
    @Latitude VARCHAR(24),
    @Longitude VARCHAR(24)
AS
BEGIN
    INSERT INTO PropertiesForSale VALUES (
        @Name, 
        @Price, 
        @Description, 
        GEOGRAPHY::STGeomFromText('POINT(' + @Latitude + ' ' + @Longitude + ')', 4326))
END

Simple enough, however, we're not going to invoke this method directly from within our web service. We've already constructed a DAL in the previous article, thus we need only create a new DAL method which can be invoked from our web service. This method is shown below.

static public bool SaveProperty(string name, decimal price, 
    string description, string latitude, string longitude)
{
    try
    {
        using (SqlConnection sqlConn = new SqlConnection(ConfigurationManager.ConnectionStrings["LocalSqlServer"].ConnectionString))
        {
            sqlConn.Open();

            using (SqlCommand cmd = new SqlCommand("VE_InsertProperty", sqlConn))
            {
                cmd.CommandType = CommandType.StoredProcedure;

                cmd.Parameters.AddWithValue("@Name", name);
                cmd.Parameters.AddWithValue("@Price", price);
                cmd.Parameters.AddWithValue("@Description", description);
                cmd.Parameters.AddWithValue("@Latitude", latitude);
                cmd.Parameters.AddWithValue("@Longitude", longitude);

                if (cmd.ExecuteNonQuery() == 0)
                    return false;
                else
                    return true;
            }
        }
    }
    catch
    {
        return false;
    }
}

We can invoke the above DAL method from our web service, as shown below.

[WebMethod]
public bool SaveLocation(string name, decimal price,
    string description, string latitude, string longitude)
{
    return Dal.SaveProperty(name, price, description, latitude, longitude);
}

Pretty straightforward!

The Application

Now that we've constructed the web service methods and implemented a DAL method to write the new home details to the SQL Server 2008 database, the only thing left is the UI portion of the application. I've created a new WebForm file named NewHome.aspx and coded the JavaScript methods that can be used to invoke the web service to query Yahoo Geocode and save the results to the database.

Prior to the home details being persisted, the user of the application must specify an address, the address will be queried against Yahoo Geocode and a pushpin will be placed on a Virtual Earth map representing the location (by the way, we could have easily used Virtual Earth to retrieve the address location!). If the location is correct, the user must supply additional details, such as the home sale price, description, etc and finally the data can be stored in the database.

Before we take a look at NewHome.aspx, let's go ahead and take a peak at the <appSettings> element of the applications web.config file.

<appSettings>
    <add key="YahooID" value="Yahoo ID"/>
    <add key="YahooGeocodeUrl" value="http://local.yahooapis.com/MapsService/V1/geocode"/>
</appSettings>

Ensure that you replace the string "Yahoo ID" with your Yahoo application ID. Let's get to the code, shall we? The method used to query Yahoo Geocode is shown below.

function geocodeAddress()
{
    var location = $get('<%=txtLocation.ClientID %>').value;
    if (location == '')
    {
        alert("Location required!");
        return false;
    }
    
    VEService.GeocodeAddress(location, getAddressSuccess, getAddressError);
}

The location to query is retrieved from a textbox and passed to the web service method GeocodeAddress(), it couldn't get much easier! Take a look at the getAddressSuccess() method below, which will be invoked when the web service proxy successfully returns.

function getAddressSuccess(e)
{            
    if (e != null)
    {
        // Clear all pins first
        var veE = $find('veextender');
        var map = veE.get_map();
        
        // Create VELatLong object
        var latLong = new VELatLong(e.Latitude, e.Longitude);
        
        // Display the preview map and insert a pin
        var shape = new VEShape(VEShapeType.Pushpin, latLong);
        shape.SetTitle(e.Name);
        shape.SetDescription(e.Description);

        // Add shape to the map
        map.AddShape(shape);
        
        // Center and zoom into the address
        map.SetCenterAndZoom(latLong, 10)
        
        // save lat and long into hidden fields
        $get('latitude').value = e.Latitude;
        $get('longitude').value = e.Longitude;
    }
    else
        alert("An error occurred. Try another address");
}

A VEShape object is constructed and placed at the latitude and longitude indicated by the queried location. Note that the Virtual Earth map is repositioned using the SetCenterAndZoom() method. Lastly, the returned latitude and longitude is temporarily persisted in HTML hidden fields.

The remaining portion of JavaScript code is quite similar, that is, after the home details have been supplied, the web service method SaveLocation() is invoked, passing the home details including the latitude and longitude found within the two HTML hidden fields to the SaveLocation() web service method. If the home has been successfully inserted into the database, the SaveLocation() method will return the Boolean value of true and we can notify the user accordingly. The JavaScript code and markup in its entirety is shown below.

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="NewHome.aspx.cs" Inherits="NewHome" MasterPageFile="~/MasterPage.master" %>
<%@ Register TagPrefix="Demo" Namespace="VirtualEarth" Assembly="__code" %>

<%@ Register assembly="AjaxControlToolkit" namespace="AjaxControlToolkit" tagprefix="cc1" %>

<asp:Content runat="server" ContentPlaceHolderID="head">
    <script type="text/javascript">
   1:  
   2:         function geocodeAddress()
   3:         {
   4:             var location = $get('<%=txtLocation.ClientID %>').value;
   5:             if (location == '')
   6:             {
   7:                 alert("Location required!");
   8:                 return false;
   9:             }
  10:             
  11:             VEService.GeocodeAddress(location, getAddressSuccess, getAddressError);
  12:         }
  13:         
  14:         function getAddressSuccess(e)
  15:         {            
  16:             if (e != null)
  17:             {
  18:                 // Clear all pins first
  19:                 var veE = $find('veextender');
  20:                 var map = veE.get_map();
  21:                 
  22:                 // Create VELatLong object
  23:                 var latLong = new VELatLong(e.Latitude, e.Longitude);
  24:                 
  25:                 // Display the preview map and insert a pin
  26:                 var shape = new VEShape(VEShapeType.Pushpin, latLong);
  27:                 shape.SetTitle(e.Name);
  28:                 shape.SetDescription(e.Description);
  29:  
  30:                 // Add shape to the map
  31:                 map.AddShape(shape);
  32:                 
  33:                 // Center and zoom into the address
  34:                 map.SetCenterAndZoom(latLong, 10)
  35:                 
  36:                 // save lat and long into hidden fields
  37:                 $get('latitude').value = e.Latitude;
  38:                 $get('longitude').value = e.Longitude;
  39:             }
  40:             else
  41:                 alert("An error occurred. Try another address");
  42:         }
  43:         
  44:         function getAddressError(e)
  45:         {
  46:             alert("Error!");
  47:         }
  48:         
  49:         function saveAddress(event)
  50:         {
  51:             var validated = Page_ClientValidate();
  52:             if (!validated)
  53:                 return false;
  54:             if ($get('latitude').value == '' || $get('longitude').value == '')
  55:             {
  56:                 alert("A location must be specified");
  57:                 return false;
  58:             }
  59:             
  60:             VEService.SaveLocation($get('<%=txtName.ClientID %>').value,
  61:                 $get('<%=txtPrice.ClientID %>').value,
  62:                 $get('<%=txtDescription.ClientID %>').value,
  63:                 $get('latitude').value,
  64:                 $get('longitude').value,
  65:                 saveSuccess,
  66:                 saveError);
  67:             
  68:             return false;
  69:         }
  70:         
  71:         function saveSuccess(e)
  72:         {
  73:             if (e)
  74:                 alert("The home was added to the database!");
  75:             else
  76:                 alert("Error!");
  77:         }
  78:         
  79:         function saveError(e)
  80:         {
  81:             alert("Error!");
  82:         }
  83:         
  84:     
</script> </asp:Content> <asp:Content runat="server" ContentPlaceHolderID="PrimaryContent"> <input type="hidden" id="latitude" /> <input type="hidden" id="longitude" /> <table style="border-collapse:collapse; width:100%"> <tr> <td>Location</td> <td> <asp:TextBox runat="server" ID="txtLocation"></asp:TextBox> <input type="button" id="btnLocate" value="Locate" onclick="geocodeAddress();" /> </td> </tr> <tr> <td colspan="2"><h3>Property details:</h3></td> </tr> <tr> <td>Name</td> <td> <asp:TextBox runat="server" ID="txtName"></asp:TextBox> <asp:RequiredFieldValidator ID="reqName" runat="server" ErrorMessage="*" ControlToValidate="txtName"></asp:RequiredFieldValidator> </td> </tr> <tr> <td>Price</td> <td> <asp:TextBox runat="server" ID="txtPrice"></asp:TextBox> <cc1:FilteredTextBoxExtender ID="txtPrice_FilteredTextBoxExtender" runat="server" Enabled="True" TargetControlID="txtPrice" FilterMode="ValidChars" FilterType="Numbers"> </cc1:FilteredTextBoxExtender> <asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ErrorMessage="*" ControlToValidate="txtPrice"></asp:RequiredFieldValidator> </td> </tr> <tr> <td style="vertical-align:top">Description</td> <td> <asp:TextBox runat="server" ID="txtDescription" Height="35px"></asp:TextBox> </td> </tr> <tr> <td align="right" colspan="2"> <asp:Button runat="server" ID="btnSave" Text="Save" OnClientClick="return saveAddress();" /> </td> </tr> </table> <h3>Location Preview</h3> <asp:Panel runat="server" ID="veMap" CssClass="veMap"></asp:Panel> <Demo:VEExtender runat="server" id="veextender" TargetControlID="veMap" Latitude="36.171830" Longitude="-115.139764" MapStyle="Hybrid"></Demo:VEExtender> </asp:Content>

When executed, the NewHome.aspx page will look similar to that of the screen shot shown below.

image

Conclusion

We've taken a quick look at the Yahoo Geocode service and discovered that its really quite simple to use. Our application utilizes the new SQL Server 2008 GEOGRAPHY data type to persist the latitude and longitude of a specific location on the world map, for more information regarding this data type refer to the previous article, Virtual Earth, SQL Server 2008 and AJAX. Finally, there are a few improvements that could be made to the application, such as visual indicators, better error handling, etc. This I will leave up to you!

Download all source files