Monday, July 13, 2009

Silverlight 3 beta 1 and Virtual Earth part 2 (The Map)

In my previous post I covered getting GEO spatial data into SQL and creating a service Silverlight can use to get the data in a usable format. In this post we are going to talk about how to create a Silverlight application to overlay geo spatial data on the Virtual Earth map. First thing first. Download the Silverlight Virtual Earth map control. Once you have done that start a new Visual Studio 2008 Silverlight project and add a reference to the Microsoft.VirtualEarth.MapControl and copy the following code into the XAML of your default window or page.
<Grid x:Name="LayoutRoot" Background="White">
    <m:Map Name="MyMap" ZoomLevel="4" Center="39.36830,-95.27340" Grid.Row="1"/>
</Grid>

Make sure you add this namespace as well for the VE add-in

xmlns:m="clr-namespace:Microsoft.VirtualEarth.MapControl;assembly=Microsoft.VirtualEarth.MapControl"

That is all you have to do. If you run your application you should now have a VE map on it. Now lets get to the fun stuff. In your code behind for the page add a using statement to Microsoft.VirtualEarth.MapControl and add a service reference to the web service you created to provide the spatial data.  Here is what my initialization method looks like:

InitializeComponent();
 
SLSpatialTypesService.SpatialTypeServiceClient serviceClient = new GovSage.SLSpatialTypesService.SpatialTypeServiceClient();
serviceClient.GetCongDistsAsync();
serviceClient.GetCongDistsCompleted += new EventHandler<GovSage.SLSpatialTypesService.GetCongDistsCompletedEventArgs>
    (serviceClient_GetCongDistsCompleted);

As you can see all I am doing is instantiating my service and setting up an event callback for when the call completes. The real work happens when this call comes back.

   1: void serviceClient_GetCongDistsCompleted(object sender, GovSage.SLSpatialTypesService.GetCongDistsCompletedEventArgs e)
   2: {
   3:     foreach (KeyValuePair<string, ObservableCollection<string>> locations in e.Result)
   4:     {
   5:         MapPolyline polyline = new MapPolyline();
   6:         polyline.Stroke = new SolidColorBrush(Colors.White);
   7:         polyline.StrokeThickness = 1;
   8:         polyline.Opacity = 0.5;
   9:         polyline.Locations = new LocationCollection();
  10:         polyline.Name = locations.Key;
  11:         
  12:         foreach (string location in locations.Value)
  13:         {
  14:             string[] latlong = location.Split(new char[] {','});
  15:             Location thisLocation = new Location(Double.Parse(latlong[0]), Double.Parse(latlong[1]));
  16:            
  17:             polyline.Locations.Add(thisLocation);
  18:         }
  19:  
  20:         switch (MyMap.Children.Count)
  21:         {
  22:             case 1:
  23:                 polyline.Fill = new SolidColorBrush(Colors.Blue);
  24:                 break;
  25:             case 2:
  26:                 polyline.Fill = new SolidColorBrush(Colors.Green);
  27:                 break;
  28:             case 3:
  29:                 polyline.Fill = new SolidColorBrush(Colors.Purple);
  30:                 break;
  31:             case 4:
  32:                 polyline.Fill = new SolidColorBrush(Colors.Red);
  33:                 break;
  34:             case 5:
  35:                 polyline.Fill = new SolidColorBrush(Colors.Yellow);
  36:                 break;
  37:             case 6:
  38:                 polyline.Fill = new SolidColorBrush(Colors.Brown);
  39:                 break;
  40:             case 7:
  41:                 polyline.Fill = new SolidColorBrush(Colors.LightGray);
  42:                 break;
  43:             default:
  44:                 polyline.Fill = new SolidColorBrush(Colors.Cyan);
  45:                 break;
  46:         }
  47:         polyline.MouseEnter += new MouseEventHandler(polyline_MouseEnter);
  48:         polyline.MouseLeave += new MouseEventHandler(polyline_MouseLeave);
  49:         MyMap.Children.Add(polyline);
  50:     }
  51: }

I get a KeyValuePair back from the web service. To create all my overlays I loop through each key value I get back. My key is my area name and the second value is an ObservableCollection of type string. In the first few lines (5-10) all I am doing is creating a new MapPolyline for each item and setting its properties and instantiating its LocationCollection. Then for each item in my ObservableCollection I loop and create a new location in my polyline. All I have to do is split the string value on the “,” and create a new location based on the latitude and longitude I am return and add it to the location collection. In my case I create 7 different MapPolyline objects each having about a hundred locations attached to them. 

At this point I have created all the objects I need to overlay on my map. Once I leave the inner foreach statement I hit a case statement that simply looks at how many children my map has and sets the polyline fill color for that object to a certain color. This allows each overlay area to have a different color.

Down at line 47 I start the code to add each MapPolyline to my map. Once you have added each as a child you should now be able to load your map and see your overlays (just make sure the latitude and longitude items you added for your locations actually matches the area you are expecting. If not your MapPolylines may get added to a completely different area of the map).

My map now looks like this and as I move in and out my MapPolylines adjust as well.

 

image

If your map scrolling is not smooth you need to do more reducing (reducing was talked about in part 1).

Silverlight 3 beta 1 and Virtual Earth part 1 (GEO Data)

I recently did some playing with Silverlight 3 beta 1 and the Virtual Earth (VE) CTP. I wanted to map out the congressional districts for Colorado on the VE map. It was a little tricky but not bad once I figured a few things out.

First thing to figure out was where to get the geo mapping data to overlay on the VE CTP map. The site with a lot of this information is the U.S. Census web sites (you can find all the Congressional district data here). I decided to download and work with the shape file (.shp). Once you have these downloaded the next trick is figuring out how to get this data into SQL Server 2008. The tool I used was Shape2SQL from SharpGIS. It works great and is pretty simple to use. There is also a CodePlex SQL Server spatial tools project that may have some helpful tools.

Now that we have our spatial data imported into SQL we can start using it. The first thing to be aware of is that .Net 3.5 does not natively understand SQLGeometry types (seems silly but it is true). To over come this I simply added the Microsoft.SqlServer.Types assembly to my project. I created a web service my Silverlight application could use to query and get the needed spatial data.

   1: [ServiceContract(Namespace = "")]
   2: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
   3: public class SpatialTypeService
   4: {
   5:     [OperationContract]
   6:     public Dictionary<string, List<string>> GetCongDists()
   7:     {
   8:         string connString = // your string here
   9:  
  10:         //Create string for SQL statement to find geometries within the map view   
  11:         StringBuilder sb = new StringBuilder();
  12:         sb.Append("SELECT Name, [GEOM].Reduce(.002) FROM [Districts]");
  13:  
  14:         //Connect to database   
  15:         SqlConnection SQLConn = new SqlConnection(connString);
  16:         SQLConn.Open();
  17:  
  18:         //Select all polygons that intersect the map extents   
  19:         SqlCommand SQLCMD = new SqlCommand(sb.ToString(), SQLConn);
  20:         SqlDataReader dr = SQLCMD.ExecuteReader();
  21:  
  22:         Dictionary<string,List<string>> locations = new Dictionary<string,List<string>>();
  23:  
  24:         while (dr.Read())
  25:         {
  26:             List<string> location = new List<string>();
  27:             SqlGeometry geo = (SqlGeometry)dr.GetValue(1);
  28:             // We start at 1 because the STPointN array cannot get a zero past to it
  29:             for (int i = 1; i < geo.STNumPoints(); i++)
  30:             {
  31:                 location.Add(geo.STPointN(i).STY.ToString() + "," + geo.STPointN(i).STX.ToString());
  32:             }
  33:             locations.Add(dr.GetValue(0).ToString(),location);
  34:         }
  35:         return locations;
  36:     }
  37: }

Most of this code is just a standard WCF contract. Lets look at the couple key areas here that deal with our GEO data. First, notice at line 12 we have our SQL statement to select the data.

  12:         sb.Append("SELECT Name, [GEOM].Reduce(.002) FROM [Districts]");

Here we are Selecting Name and GEOM data for all the districts we have. Notice the “Reduce(.002)” call we make. This Reduce function is key to keep performance of mapping this data fast. For Reduce you can pass in a number between 1 and 0. This call reduces the total number of mapping points SQL returns to us. The closer to 1 you get the less mapping points are returned. I used the Shape2SQL SQL Spatial tool to quickly play with the number and see what the returned mapping shape is (screen shapes below). The key here is to reduce as much as possible without changing the shape of what is returned. In my case .002 takes me from over a thousand points per zone to around 100 points. This makes zooming in and out on the VE CTP perform A LOT better.

imageHere is what my areas look like at .002:

 

 

 

 

image

Here is what it looks like at .1. You can see how my shapes have changed a lot and in my case to much. Depending on what you are trying to do, just play with the reduce function until you get the points as low as you can.

 

 

Now for the work we do to get all the values ready and in a form our Silverlight app can use (Lines 24 – 34).

Dictionary<string,List<string>> locations = new Dictionary<string,List<string>>();
 
while (dr.Read())
{
    List<string> location = new List<string>();
    SqlGeometry geo = (SqlGeometry)dr.GetValue(1);
    // We start at 1 because the STPointN array cannot get a zero past to it
    for (int i = 1; i < geo.STNumPoints(); i++)
    {
        location.Add(geo.STPointN(i).STY.ToString() + "," + geo.STPointN(i).STX.ToString());
    }
    locations.Add(dr.GetValue(0).ToString(),location);
}

I created a dictionary item to hold the name of each area and a list of its points. Once I run my select I loop through the datareader for each record and create a collection of all its points. To do this I load up the GEOM data as a SqlGeometry type. I can then see how many STNumPoints each GEOM object has and get the X and Y value for each point. I simply load that up as a string in the list that is ‘X,Y’ format.

This will get you in a position for your Silverlight application to consume the data. In my next post I will cover what my Silverlight application is doing.

Here is a screenshot of where we are going though:

image