I recently put together a demo for The Big Event and I wanted to document how I developed this demo. The Club Site Starter Kit is a free download for Visual Studio that comes with source code. It contains the basics for a club site including Events, News, Photos, and Links. In looking at the Events component, there is a locations function where a user can enter in an address for the event. My goal was to extend the UI to provide a map view of the events in the system on the front page.
I based on lot of this work on a posting from Beth Massi which shows how to map some of the Northwind sample accounts in Virtual Earth using VB.NET which has some killer language features around XML.
To create a new Club Site, open Visual Studio and create a new website. After you have installed the Club Site Starter Kit you will have a new project type under "My Templates". If you create the site it will run right out of the box. At this point you probably want to go into the ASP.NET configuration (under the Website menu) and create an administrator account. The admin account will be required to actually update data on the site.
Now that we have the site up and running lets go through the modifications.
Extend the Database
The club site starter kit comes with a database that contains a table for locations. I added two more fields to the database: lat and long both as varchar(50). These fields will store the latitude and longitude associated with the address.
Get the Latitude and Longitude when creating or updating an address
One of the things that Beth discovered when putting together her sample was a website that will geocode (convert) an address to latitude and longitude using a Rest based web service. The first step was to leverage this web service whenever a new or updated address is entered into the site on the locations.aspx page. Since the page is bound to the database, I created two additional controls on the page that were hidden and held the latitude and longitude into both the insert and update views. I also trapped the OnTextChanged event.
<asp:TextBox Text='<%# Bind("Address") %>' runat="server" ID="TextBox1" Rows="10" TextMode="MultiLine" Width="500px" Height="166px" OnTextChanged="TextBox1_TextChanged"></asp:TextBox>
<asp:TextBox Text='<%# Bind("lat") %>' runat="server" ID="txtLat" Visible="false" />
<asp:TextBox Text='<%# Bind("long") %>' runat="server" ID="txtLong" Visible ="false" />
In the page server code I added the following C# that handles the OnTextChanged event
protected void TextBox1_TextChanged(object sender, EventArgs e)
{
TextBox lng = (TextBox)(FormView1.FindControl("txtLong"));
TextBox lat = (TextBox)(FormView1.FindControl("txtLat"));
TextBox address = (TextBox)(FormView1.FindControl("TextBox1"));
var url = "http://geocoder.us/service/rest/?address=" + Server.UrlEncode(address.Text);
XNamespace nsGeo = "http://www.w3.org/2003/01/geo/wgs84_pos#";
XElement geo;
try
{
geo = XElement.Load(url);
}
catch (Exception ex)
{
//in production put in some better exception handling
throw ex;
}
lng.Text = geo.Element(nsGeo + "Point").Element(nsGeo + "long").Value;
lat.Text = geo.Element(nsGeo + "Point").Element(nsGeo + "lat").Value;
}
The above code encodes the address and calls the geocoder web service to obtain the latitude and longitude. If you want to try out the service you can call it directly via your browser (for example http://geocoder.us/service/rest/?address=1600%20Pennsylvania%20Avenue%20NW%20Washington,%20DC%2020500 will give you the latitude and longitude of the White House).
The last two lines extract the latitude and longitude from the resulting XML using LINQ to XML. I find it much easier than trying to traverse the DOM but you can pull that information using traditional DOM code.
One other point is that I would probably make is that this code would need to have some additional error processing when an invalid address is entered or if the geocoding service is not available. Currently, if this particular geocoder service cannot convert the address the Club Site application will just leave these fields blank and the point will not show up on the map. Also, this particular service isn't always as accurate as I would like (but it is free), I have had some points be off by a couple hundred yards.
Create a GeoRSS Feed
There are a couple of ways to integrate your custom data with Virtual Earth but I personally like the ability to integrate a GeoRSS feed. If you want to find out more about programming against Virtual Earth check out the Interactive SDK. Since I have the data stored in a database I created the feed using the following code:
using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
public partial class GeoRSS : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Response.ContentType = "text/xml";
XElement geoRSS = GetGeoRSS();
Response.Write(geoRSS.ToString());
}
private XElement GetGeoRSS()
{
NorthwindDataContext db = new NorthwindDataContext();
var events = from e in db.Events
select new { e.id, e.title, e.description, e.starttime, e.endtime, e.LocationDetail.address, e.LocationDetail.Location_title, e.LocationDetail.lat, e.LocationDetail.@long };
XNamespace nsGeo = "http://www.w3.org/2003/01/geo/wgs84_pos#";
XNamespace nsGeorss = "http://www.georss.org/georss";
XNamespace nsGml = "http://www.opengis.net/gml";
XElement xmlFeed = new XElement("rss",
new XAttribute("version", 2.0),
new XAttribute(XNamespace.Xmlns + "geo", nsGeo),
new XAttribute(XNamespace.Xmlns + "georss", nsGeorss),
new XAttribute(XNamespace.Xmlns + "gml", nsGml),
new XElement("channel",
new XElement("title", "Club Events Feed"),
new XElement("link", Request.Url.AbsoluteUri),
new XElement("description", "Events coming up...")
)
);
XElement xmlChannel = xmlFeed.Element("channel");
foreach (var row in events)
{
xmlChannel.Add(
new XElement("item",
new XElement("title", row.title),
new XElement("link",
new XAttribute("rel", "via"),
new XAttribute("href", "http://localhost:1589/ClubWebSite1/Events_view.aspx?EventID=" + row.id.ToString())
),
new XElement("description", row.description),
new XElement("content", (string)BuildContent(row.description, row.starttime, (DateTime)row.endtime, row.address, row.Location_title),
new XAttribute("type", "html")),
new XElement(nsGml + "Point",
new XElement(nsGml + "pos", row.lat + " " + row.@long)
)
)
);
}
return xmlFeed;
}
private string BuildContent(string description, DateTime starttime, DateTime endtime, string address, string locname)
{
string content;
string when = starttime.ToLongDateString() + " " + starttime.ToShortTimeString() + " - " + endtime.ToShortTimeString();
content = "<b>" + when + "</b><br/>" + locname + "<br>" + address + "<br/><br/>" + description;
return content;
}
}
Basically the above code uses LINQ to SQL to pull the data out of the database and LINQ to XML to format the XML output. You can create the XML in any fashion that you would like as long as it conforms to the GeoRSS standard. The other reason I like this approach is that I can also extend this approach to filter by all sorts of criteria via querystring (if I wanted to filter by date for example).
The actual georss.aspx html code is below. Notice that I am not caching any information (because I use this in demos). In a real environment I would tune the cache a little differently.
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="GeoRSS.aspx.cs" Inherits="GeoRSS" %>
<%Response.Expires = -1; %>
Modify the Home Page to Include the Map
At this point the heavy lifting is complete. Now all that is left is to actually place the map on the home page and bind it to the GeoRSS feed that we created. The one difficulty in this example is that the Club Site Starter Kit uses master pages and content pages. This causes two issues in that we cannot easily trap the <body> tag OnLoad event and we have to programmatically inject script into the <head> secion.
I added the following C# code to inject the include script for the map control into the <head> section of the page:
protected void Page_Load(object sender, EventArgs e)
{
Page.ClientScript.RegisterClientScriptInclude("VEScript", "http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6");
}
Then I added the map to the appropriate portion of the page. You can see that I added a <br> tag with an OnLoad event to trigger the map load.
<br onload="GetMap();" />
<div id="myMap" style="position:relative; width:446px ; height:400px"/>
Finally, I added a script for the GetMap event to render the map and wire it up to the GeoRSS feed.
<script type="text/javascript">
var map = null;
self.setTimeout("GetMap()", 1);
function GetMap()
{
map = new VEMap('myMap');
map.SetDashboardSize(VEDashboardSize.Small);
map.LoadMap();
var layer = new VEShapeLayer();
var veLayerSpec = new VEShapeSourceSpecification(VEDataType.GeoRSS, "georss.aspx", layer);
map.ImportShapeLayerData(veLayerSpec, null);
}
</script>
The one thing I had to add that you won't find in the Interactive SDK is the self.setTimeout call. This forces this function to load after the entire body is done rendering.
Here is the final result: