
Fig 1 – SQL Server 2008 Spatial and Silverlight MapControl
Microsoft SQL Server 2008 introduces some spatial capabilities. Of course anytime Microsoft burps the world listens. It isn’t perhaps as mature as PostGIS, but it isn’t as expensive as Oracle either. Since it’s offered in a free Express version I wanted to give it a try, connecting the Silverlight MapController CTP with some data in MS SQL Server 2008. Here is the reference for SQL Server 2008 with new geography and geometry capabilities: Transact-SQL
To start with I needed some test data in .shp format.
The new Koordinates Beta site has a clean interface to some common vector data sources. I signed up for a free account and pulled down a version of the San Francisco Bay Area bus stops, bikeways, and transit stations in Geographic WGS 84 (EPSG:4326) coordinates as .shp format. Koordinates has built a very nice data interface and makes use of Amazon AWS to provide their web services. They include choices for format as well as a complete set of EPSG coordinate systems that make sense for the chosen data source. I selected Geographic WGS 84 (EPSG:4326) because one area SQL Server is still lacking is EPSG coordinate system support. EPSG:4326 is the one supported coordinate system in the Transact SQL geography and there is no transform function.

Fig 2 -Koordinates SF Bay Bus Stop data
With data downloaded I can move on to import. Morten Nielson’s SharpGIS has some easy to use tools for importing .shp files into the new MS SQL Server 2008 spatial. Windows Shape2Sql.exe, like many windows apps, is easier to use for one at a time file loading with its nice GUI, at least comparing to the PostgreSQL/PostGIS shp2pgsql.exe loader. However, batch loading might be a bit painful without cmd line and piping capability. SQL Server Spatial Tools

Fig 3 – Shape2Sql from sharpgis.net
Curiously SQL Server offers two data types, the lat,long spatial data type, geography, and the planar spatial data type, geometry. SRIDs can be associated with a record, but currently there is no transform capability. Hopefully this will be addressed in future versions. Looking ahead to some comparisons, I used Shape2SQL to load Bay Area Bus Stops as geometry and then again as geography. In both cases I ended up with a field named “geom”, but as different data types. The new geo data types hold the spatial features. The data load also creates a spatial index on the geom field. Once my data is loaded into MS Sql Server I need to take a look at it.
Using geom.STAsText() to show WKT, I can look inside my geography field, “geom”, and verify the loading:
SELECT TOP 1000 [ID]
,[AGENCYNAME]
,[REGIONNAME]
,[JURISDICTI]
,[ASSET_NAME]
,[RuleID]
,[CLASS]
,[geom]
,geom.STAsText()as WKT
FROM [TestShp].[dbo].[bay-area-transit-stations]
ID AGENCYNAME REGIONNAME JURISDICTI ASSET_NAME RuleID CLASS geom WKT
1 Caltrain San Mateo San Bruno San Bruno Station 1 Rapid Rail Station 0xE6100000010C003163A9BCCF4240DB58CB7F109A5EC0 POINT (-122.40725703104128 37.622944997247032)

Fig 4 – SQL Server showing a Geography Display (note selection of projections for map view)
SQL Server 2008 Management Studio includes a visual display tab as well as the normal row grid. Geography data type offers a choice of 4 global projections and affords some minimalistic zoom along with a lat,long grid for verifying the data. But geometry data type ignores projection. The geometry spatial result offers a grid based on the x,y extents of the data selection. I didn’t find a way to show more than one table at at a time in the spatial view.
Here is a list of some helpful geometry functions:
OGC Methods on Geometry Instance
And here is a list of geography functions:
OGC Methods on Geography Instance
It is worth noting that there are differences in the method lists between geometry and geography:
Geography OGC Methods Geometry OGC Methods
STArea STArea
STAsBinary STAsBinary
STAsText STAsText
STBuffer STBoundary
STDifference STBuffer
STDimension STCentroid
STDisjoint STContains
STDistance STConvexHull
STEndpoint STCrosses
STEquals STDifference
STGeometryN STDimension
STGeometryType STDisjoint
STIntersection STDistance
STIntersects STEndpoint
STIsClosed STEnvelope
STIsEmpty STEquals
STLength STExteriorRing
STNumGeometries STGeometryN
STNumPoints STGeometryType
STPointN STInteriorRingN
STSrid STIntersection
STStartPoint STIntersects
STSymDifference STIsClosed
STUnion STIsEmpty
STIsRing
STIsSimple
STIsValid
STLength
STNumGeometries
STNumInteriorRing
STNumPoints
STOverlaps
STPointN
STPointOnSurface
STRelate
STSrid
STStartPoint
STSymDifference
STTouches
STUnion
STWithin
STX
STY
Extended Geography Methods Extended Geometry methods
AsGml AsGml (geometry Data Type)
AsTextZM AsTextZM (geometry Data Type)
BufferWithTolerance BufferWithTolerance (geometry Data Type)
InstanceOf InstanceOf (geometry Data Type)
IsNull Filter (geometry Data Type)
Lat IsNull (geometry Data Type)
Long M (geometry Data Type)
M MakeValid (geometry Data Type)
NumRing Reduce (geometry Data Type)
RingN ToString (geometry Data Type)
ToString Z (geometry Data Type)
Z
OGC Geography Static Methods OGC Static Geometry methods
STGeomFromText STGeomFromText (geometry Data Type)
STPointFromText STPointFromText (geometry Data Type)
STLineFromText STLineFromText (geometry Data Type)
STPolyFromText STPolyFromText (geometry Data Type)
STMPointFromText STMPointFromText (geometry Data Type)
STMLineFromText STMLineFromText (geometry Data Type)
STMPolyFromText STMPolyFromText (geometry Data Type)
STGeomCollFromText STGeomCollFromText (geometry Data Type)
STGeomFromWKB STGeomFromWKB (geometry Data Type)
STPointFromWKB STPointFromWKB (geometry Data Type)
STLineFromWKB STLineFromWKB (geometry Data Type)
STPolyFromWKB STPolyFromWKB (geometry Data Type)
STMPointFromWKB STMPointFromWKB (geometry Data Type)
STMLineFromWKB STMLineFromWKB (geometry Data Type)
STMPolyFromWKB STMPolyFromWKB (geometry Data Type)
STGeomCollFromWKB (geometry Data Type)
Extended Static Geography Methods Extended Static Geometry Methods
GeomFromGML GeomFromGML
Null Null
Parse Parse
Point Point
Here is an article with some background material on geography vs geometry – SQL Server Spatial Data
Now that we have some spatial data loaded it’s time to see about hooking up to the new Silverlight MapController CTP. The basic approach is to access the spatial data table through a service on the Web side. A service reference can then be added to the Silverlight side, where it can be picked up in the C# code behind for the MapControl xaml page. This is familiar to anyone using Java to feed a Javascript client. The Database queries happen server side using a servlet with jdbc, and the client uses the asynchronous query result callback to build the display view.
Continuing with the new Silverlight MapControl CTP, I took a look at this tutorial: Johannes Kebeck Blog
However, the new geo data types are not supported through Linq designer yet. This means I wasn’t able to make use of Linq OR/M because of the geom fields. I then switched to an ADO.NET Data Service model, which appears to work, as it produces the ado web service and allows me to plug in my GeoModel.edmx GeoEntities into the new auto generated GeoDataService.svc like this:
GeoDataService : DataService<GeoEntities>
Using the service endpoint call
“http://localhost:51326/GeoDataService.svc/bay_area_transit_stations(1)”
returns the correct entry, but minus the all important geom field. Once again I’m thwarted by OR mapping, this time in EF. Examining the auto generated GeoModel.designer.cs reveals that geom field class is not generated. Perhaps this will be changed in future releases.
<?xml version=”1.0″ encoding=”utf-8″ standalone=”yes”?>
<entry xml:base=”http://localhost:51326/GeoDataService.svc/”
xmlns:d=”http://schemas.microsoft.com/ado/2007/08/dataservices”
xmlns:m=”http://schemas.microsoft.com/ado/2007/08/dataservices/metadata”
xmlns=”http://www.w3.org/2005/Atom”>
<id>http://localhost:51326/GeoDataService.svc/bay_area_transit_stations(1)</id>
<title type=”text”></title>
<updated>2009-04-16T21:10:50Z</updated>
<author>
<name />
</author>
<link rel=”edit” title=”bay_area_transit_stations” href=” bay_area_transit_stations(1)” />
<category term=”GeoModel.bay_area_transit_stations”
scheme=”http://schemas.microsoft.com/ado/2007/08/dataservices/scheme” />
<content type=”application/xml”>
<m:properties>
<d:ID m:type=”Edm.Int32″>1</d:ID>
<d:AGENCYNAME>Caltrain</d:AGENCYNAME>
<d:REGIONNAME>San Mateo</d:REGIONNAME>
<d:JURISDICTI>San Bruno</d:JURISDICTI>
<d:ASSET_NAME>San Bruno Station</d:ASSET_NAME>
<d:RuleID m:type=”Edm.Int64″>1</d:RuleID>
<d:CLASS>Rapid Rail Station</d:CLASS>
</m:properties>
</content>
</entry>
So neither Linq OR/M or ADO.net OR/M has caught up with the SQL Server 2008 data types yet. Apparently Linq is competing internally wtih ADO.NET EF, see Is LINQ to SQL Dead? I gather going forward .NET 4.0 will be emphasizing Entity Framework, but as far as spatial data types auto generated code isn’t there yet.
Back to manual code. I can add a Silverlight-enabled WCF service to my GeoTest.Web that connects to the database and then pass the results back to Silverlight using a Data Service Reference. From that point I can use the result inside Page.cs to populate a MapLayer on the map interface with Ellipse shapes representing the transit stops or MapPolyline for Bikeways. I know there must be a more sophisticated approach to this process, but for simplicity I’ll just pass results back to the Silverlight page via strings.
Here is a WCF service:
using System;
using System.Windows.Forms;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using Microsoft.SqlServer.Types;
namespace GeoTest.Web
{
[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class SFBayDataService
{
[OperationContract]
// pass in the current map bounds for an intersect query
public List GetTransitStops(double llx, double lly, double urx, double ury)
{
string query = "SELECT [AGENCYNAME],[REGIONNAME],[JURISDICTI],[ASSET_NAME],[geom] FROM [TestShp].[dbo].[bay-area-transit-stations] WHERE geom.STIntersects(geography::STGeomFromText('POLYGON(("+llx+" "+lly+","+ llx+" "+ury+","+urx+" "+ury+","+urx+" "+lly+","+llx+" "+lly+"))', 4326))=1;";
string connStr = ConfigurationManager.ConnectionStrings["TestShpConnectionString"].ConnectionString;
StringBuilder sb = new StringBuilder();
List records = new List();
try
{
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand(query, conn))
{
using (SqlDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
sb.Length = 0;
sb.Append(rdr["AGENCYNAME"].ToString());
sb.Append(";" + rdr["REGIONNAME"].ToString());
sb.Append(";" + rdr["JURISDICTI"].ToString());
sb.Append(";" + rdr["ASSET_NAME"].ToString());
SqlGeography geo = (SqlGeography)rdr["geom"];
sb.Append(";" + (double)geo.Lat);
sb.Append(";" + (double)geo.Long);
records.Add(sb.ToString());
}
rdr.Close();
}
}
conn.Close();
}
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}
return records;
}
[OperationContract]
public List GetBusStops(double llx, double lly, double urx, double ury)
{
string query = "SELECT [AGENCYID], [AGENCY], [ROUTEID], [NAME], [geom] FROM [TestShp].[dbo].[bus-geography] WHERE geom.STIntersects(geography::STGeomFromText('POLYGON((" + llx + " " + lly + "," + llx + " " + ury + "," + urx + " " + ury + "," + urx + " " + lly + "," + llx + " " + lly + "))', 4326))=1;";
string connStr = ConfigurationManager.ConnectionStrings["TestShpConnectionString"].ConnectionString;
StringBuilder sb = new StringBuilder();
List records = new List();
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand(query, conn))
{
using (SqlDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
sb.Length = 0;
sb.Append(rdr["AGENCYID"].ToString());
sb.Append(";" + rdr["AGENCY"].ToString());
sb.Append(";" + rdr["ROUTEID"].ToString());
sb.Append(";" + rdr["NAME"].ToString());
/*
SqlGeometry geo = (SqlGeometry)rdr["geom"];
sb.Append(";" + (double)geo.STY);
sb.Append(";" + (double)geo.STX);
* */
SqlGeography geo = (SqlGeography)rdr["geom"];
sb.Append(";" + (double)geo.Lat);
sb.Append(";" + (double)geo.Long);
records.Add(sb.ToString());
}
rdr.Close();
}
}
conn.Close();
return records;
}
}
[OperationContract]
public List GetBikeways(double llx, double lly, double urx, double ury)
{
string query = "SELECT [ID],[CLASS],[geom] FROM [TestShp].[dbo].[bay-area-bikeways] WHERE geom.STIntersects(geography::STGeomFromText('POLYGON((" + llx + " " + lly + "," + llx + " " + ury + "," + urx + " " + ury + "," + urx + " " + lly + "," + llx + " " + lly + "))', 4326))=1;";
string connStr = ConfigurationManager.ConnectionStrings["TestShpConnectionString"].ConnectionString;
StringBuilder sb = new StringBuilder();
List records = new List();
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand(query, conn))
{
using (SqlDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
sb.Length = 0;
sb.Append(rdr["ID"].ToString());
sb.Append(";" + rdr["CLASS"].ToString());
SqlGeography geo = (SqlGeography)rdr["geom"];
for (int i = 1; i
Notice that the STIntersects query needs to use a matching SRID in the view bounds Polygon to get a result. I also noticed that using a geometry data type causes an inordinately long query time. In my test with Bus Stops up to 5 sec was required for a query that was only in the sub second range for a geography data type with the same STIntersects query. After looking over the statistics for the two versions, I could see that the geometry version loops through the entire 79801 records to retrieve 32 rows, while the geography version uses only 4487 loops to get the same result. Perhaps I'm missing something but the geometry index doesn't appear to be working!
Here is the Page.cs that consumes the WCF service result:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using Microsoft.VirtualEarth.MapControl;
using System.ServiceModel;
using GeoTest.SFBayDataServiceReference;
namespace GeoTest
{
public partial class Page : UserControl
{
private SFBayDataServiceClient svc;
public Page()
{
InitializeComponent();
svc = GetServiceClient();
}
private SFBayDataServiceClient GetServiceClient()
{
Uri uri = new Uri(HtmlPage.Document.DocumentUri, "SFBayDataService.svc");
EndpointAddress address = new EndpointAddress(uri);
return new SFBayDataServiceClient("*", address);
}
private void CheckBox_click(object sender, RoutedEventArgs e)
{
CheckBox cb = sender as CheckBox;
switch (cb.Content.ToString()){
case "Transit Stops":{
if ((bool)cb.IsChecked){
MyMap.ViewChangeEnd += TransitStops_ViewChanged;
LocationRect bounds = MyMap.GetBoundingRectangle();
svc.GetTransitStopsCompleted += svc_GetTransitStopsCompleted;
svc.GetTransitStopsAsync(bounds.Southeast.Longitude,
bounds.Southeast.Latitude,
bounds.Northwest.Longitude,
bounds.Northwest.Latitude);
}
else {
MyMap.ViewChangeEnd -= TransitStops_ViewChanged;
(MyMap.FindName("TransitStops") as MapLayer).Children.Clear();
}
break;
}
case "Bus Stops":
{
if ((bool)cb.IsChecked)
{
MyMap.ViewChangeEnd += BusStops_ViewChanged;
LocationRect bounds = MyMap.GetBoundingRectangle();
svc.GetBusStopsCompleted += svc_GetBusStopsCompleted;
svc.GetBusStopsAsync(bounds.Southeast.Longitude,
bounds.Southeast.Latitude,
bounds.Northwest.Longitude,
bounds.Northwest.Latitude);
}
else
{
MyMap.ViewChangeEnd -= BusStops_ViewChanged;
(MyMap.FindName("BusStops") as MapLayer).Children.Clear();
}
break;
}
case "Bikeways":
{
if ((bool)cb.IsChecked)
{
MyMap.ViewChangeEnd += Bikeways_ViewChanged;
LocationRect bounds = MyMap.GetBoundingRectangle();
svc.GetBikewaysCompleted += svc_GetBikewaysCompleted;
svc.GetBikewaysAsync(bounds.Southeast.Longitude,
bounds.Southeast.Latitude,
bounds.Northwest.Longitude,
bounds.Northwest.Latitude);
}
else
{
MyMap.ViewChangeEnd -= Bikeways_ViewChanged;
(MyMap.FindName("Bikeways") as MapLayer).Children.Clear();
}
break;
}
}
}
private void TransitStops_ViewChanged(object sender, MapEventArgs e)
{
(MyMap.FindName("TransitStops") as MapLayer).Children.Clear();
LocationRect bounds = MyMap.GetBoundingRectangle();
svc.GetTransitStopsCompleted += svc_GetTransitStopsCompleted;
svc.GetTransitStopsAsync(bounds.Southeast.Longitude,
bounds.Southeast.Latitude,
bounds.Northwest.Longitude,
bounds.Northwest.Latitude);
}
private void svc_GetTransitStopsCompleted(object sender, GetTransitStopsCompletedEventArgs e)
{
string[] fields;
if (e.Error == null)
{
MapLayer lyr = (MapLayer)MyMap.FindName("TransitStops");
for (int i=0;i
This bit of code turned out to be very useful:
private SFBayDataServiceClient GetServiceClient()
{
Uri uri = new Uri(HtmlPage.Document.DocumentUri,
"SFBayDataService.svc");
EndpointAddress address = new EndpointAddress(uri);
return new SFBayDataServiceClient("*", address);
}
WCF services are easy to create and use in Visual Studio, however, deployment is a bit of a trick. The above code snippet ensures that the endpoint address follows you over to the deployment server. I have still not mastered WCF deployment and spent a good deal of trial and error time getting it working. Deployment turned out to be the most frustrating part of the process.
Now, I have a Silverlight map display with fully event driven geometry similar to SVG. ToolTips give automatic rollover info, but it is also possible to add mouse events to completely customize interaction with individual points, polylines, and polygons. I expect that this will result in more interesting event driven map overlays. Unfortunately, large numbers of features slow down map interactions so a better approach is to use a Geoserver WMS tile cache for zoom levels down to a reasonable level and then switch to shape elements.
Summary
It is still early in the game for SQL Server 2008 spatial. There are a few holes and version 1.0 is not really competitve with PostGIS in capability, but performance is decent using the geography data type. The combination of Silverlight MapControl and SQL Server spatial is good for tightly coupled webapps. However, these days OGC standards implemented by OGC servers make decoupled viewer/data stores very easy to develop. I would choose a decoupled architecture in general, unless there are constraints requiring use of Microsoft products. Future releases will likely add OR/M auto generated code for geography and geometry data types. Also useful would be a complete multigeometry capability and general EPSG support.
The nice thing is the beauty of a Silverlight MapControl and the ability to completely customize overlays with event driven interaction at the client using C# instead of javascript.