If you’ve ever tried to find a way to display an ESRI ShapeFile on a Virtual Earth map, you’ll have probably noticed how little information there is on the Internet about how to accomplish this apparently arduous task.
As a matter of fact, I needed to do exactly this while adding some collateral features to the software Serena and I are developing for our thesis, but after some Googling, I found that there was (and there still is) almost no useful information on how to achieve integration between Virtual Earth and ESRI Shapefiles.
So I had to figure out what I judge a rather clean solution by myself. After all, I thought I could fill the void on this topic and help someone else avoid to switch “lazy mode” off by writing an article about it 😉
By the way, with the technique I’m describing, you’re going to be able to not only display Shapefiles as layers on Virtual Earth, but also interact with them in countless ways: extract data from them, show data associated with a shape when you click on it on the map, even modify or create new Shapefiles (Although only showing and querying the map are going to be covered in this post). All of this care of SharpMap, one of the best (if not the best) open source .NET libraries for GIS.
Actually, SharpMap does much, much more than this… it even has a performing rendering engine for spatial data, so it really is an understatement to think of it as a mere instrument to get ESRI data into our Virtual Earth maps. In fact I strongly recommend to go and give a look at the SharpMap project page on CodePlex.
SharpMap versions
SharpMap 2.0 is currently in beta stage. The currently stable release is 0.9. Version 2.0 beta is not radically different from version 0.9, at least for the parts of the library that we’re going to use, so feel free to resort to the stable version if you feel more at ease, but I thought worthy to use version 2.0 beta in my project because of a rather important addition, i.e. the possibility to write on Shapefiles as well as read them, which to my knowledge wasn’t present in the 0.9 version. Obviously, if your application doesn’t require to modify or create Shapefiles, you can safely fall back to the 0.9 version.
The idea.
Let’s suppose that we have a Shapefile containing polygons which represent the administrative regions of the United Kingdom, and that we want to overlay that on the map.
First of all we need to extract the polygon data from the Shapefile, and convert all the polygons to a format readable by Virtual Earth. Each “part” in the Shapefile will become a “polygon shape” in Virtual Earth. In this phase, we will leverage SharpMap’s ability to parse Shapefiles to .NET objects.
public List<Shape> LoadShapefileLayer(string filePath) {
Random random = new Random((int)DateTime.Now.Ticks);
// SharpMap's Shapefile reader / writer
ShapeFileProvider sf = null;
try {
sf = new ShapeFileProvider(filePath);
sf.Open(false);
if (sf.ShapeType != ShapeType.Polygon)
return null;
// Initializing the list of Shapes to be returned
List<Shape> layer = new List<Shape>();
// Retrieving all the geometries in the Shapefile
BoundingBox ext = sf.GetExtents();
IEnumerable<Geometry> geometries = sf.ExecuteGeometryIntersectionQuery(ext);
// Iterating through all the geometries in the shapefile, converting them
// to Shape objects and adding them to the output list
foreach (Geometry geometry in geometries) {
List<PointF> points = new List<PointF>();
Polygon polygon = geometry as SharpMap.Geometries.Polygon;
/* Verify that the geometry is actually a polygon. If it is not, just skip
* it. Multipolygon support is not yet added */
if (polygon != null) {
foreach (var vertex in polygon.ExteriorRing.Vertices)
points.Add(new PointF((float)vertex.X, (float)vertex.Y));
/* Each shape is assigned a random colour, to create
* the effect of a political map */
Color randomColour = Color.FromArgb(75, random.Next(255),
random.Next(255), random.Next(255));
layer.Add(new Shape(points, randomColour));
}
}
return layer;
} finally {
if (sf != null)
sf.Close();
}
}
This example only covers the importing of Polygon-based Shapefiles, but it can easily be extended to support Polylines and Points
Next, we need to make this data available to the Virtual Earth Map. We can do this by leveraging ASP.NET AJAX ScriptServices. To make a long story short, in order to use the converted data with the Virtual Earth API, on the client side, we need to create an ASP.NET web service, and decorate it with the [ScriptService] attribute. This will instruct the ASP.NET engine to create a Javascript helper class and make this method callable from client code in the page.
If you want to know more about the details of this process, the underlying communication protocol and different ways to leverage it, I recommend you to read my series of posts on “How to integrate JSON web services in an ASP.NET AJAX web application” here. You could also find my previous post on how to create a simple Virtual Earth Mashup useful. You can find it here
[WebService]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class MapService : WebService {
[WebMethod]
public List<Shape> GetMapData(string fileName) {
// ShapefileReader is a simple class containing
// the Shapefile reading and querying methods
ShapefileReader sr = new ShapefileReader();
return sr.LoadShapefileLayer(Server.MapPath("~/Maps/" + fileName));
}
}
This web method will be callable with extreme simplicity from the client side:
function LoadShapefile(fileName)
{
/* The shapefileName global variable holds the filename of the
* last loaded Shapefile for future reference (i.e. querying the map) */
shapefileName = fileName;
/* Here we call the GetMapData asmx ScriptService, passing fileName as
* a parameter. GetESRIData_success is a callback that will be executed
* upon successful completion of the method call, and will be passed the
* return data as parameter. onFailed, on the other side, will be executed
* in case of failed request */
MapService.GetMapData(fileName, GetESRIData_success, onFailed);
// MessageBox is a function that displays a nice informative box on the map
MessageBox(true, "Please wait while retrieving Shapefile data...");
}
Now that the data is available to the client side, we need to go through all the Shape objects and actually convert them to Virtual Earth shapes.
In case of successful completion of the method call, the GetESRIData_success callback will invoke the AddLayer() method, passing it the received object; AddLayer() will perform the conversion before actually drawing the shapes on the map.
function GetESRIData_success(e) {
if (e != null)
AddLayer(e);
}
function onFailed(e) {
MessageBox(
true,
"Error while connecting to the remote web service. Please try again later.",
5000);
}
function AddLayer(jsonLayer) {
var polygons = ConvertShapes(jsonLayer);
for (var i = 0; i < polygons.length; i++) {
DrawPolygon(polygons[i]);
}
MessageBox(true,
String.format("{0} polygons drawn on the map", polygons.length),
2000);
}
The conversion from custom list of deserialized .NET Shape objects to array of actual Virtual Earth VEShape objects will be performed in two steps: first of all, the ConvertShapes() method will convert the list of deserialized Shape objects to an array of JavaScript “polygon” objects, each containing an array of VELatLong objects and a colour for the shape:
function ConvertShapes(layer) {
var polygons = new Array();
for (var i = 0; i < layer.length; i++) {
var polygon = new Object();
var llpoints = new Array();
for (var j = 0; j < layer[i].Points.length; j++) {
llpoints.push(new VELatLong(layer[i].Points[j].Y, layer[i].Points[j].X));
}
var veCol = new VEColor(layer[i].Colour.R, layer[i].Colour.G,
layer[i].Colour.B, layer[i].Colour.A / 255);
polygon.Points = llpoints;
polygon.Colour = veCol;
polygons.push(polygon);
}
return polygons;
}
Finally, for each of the JavaScript polygon objects, a new VEShape object is created and added to the map, care of the DrawPolygon() method.
function DrawPolygon(polygon) {
var shape = new VEShape(VEShapeType.Polygon, polygon.Points);
shape.SetFillColor(polygon.Colour);
shape.SetLineColor(polygon.Colour);
shape.SetLineWidth(1);
shape.HideIcon();
map.AddShape(shape);
}
Finally, this is what we get:
Pretty neat, isn’t it? 😉
Obviously, the shapes become more detailed as you zoom in:
Querying.
Now what if we want to access the info associated with each part (i.e. shape) in the Shapefile?
Luckily, SharpMap comes once again in our help, making this very easy. When provided with a bounding box, the library’s ExecuteIntersectionQuery() method returns a DataSet containing the information associated with the shapes that intersect it. All we need to do to get this info to the client is create another ScriptService, and invoke it when the user clicks on the map, providing the Latitude and Longitude of the clicked point.
This is the method that actually performs the query on the Shapefile:
public string QueryPoint(string filePath, double lat, double lon) {
ShapeFileProvider sf = null;
try {
sf = new ShapeFileProvider(filePath);
if (!sf.IsOpen)
sf.Open(false);
/* This is the SharpMap object that will contain the query results */
FeatureDataSet ds = new FeatureDataSet();
/* This is the extents of the map upon which we want to perform the query.
* In our case, it will simply be the point where the user clicked. */
BoundingBox bbox = new SharpMap.Geometries.Point(lon, lat).GetBoundingBox();
/* This actually executes the query and puts the results into the
* FeatureDataSet */
sf.ExecuteIntersectionQuery(bbox, ds);
DataTable table = ds.Tables[0];
if (table.Rows.Count < 1)
return null;
/* Here we format the query results in a simple HTML table to be shown in
* the MessageBox on the map for demo purposes */
StringBuilder sb = new StringBuilder();
sb.AppendLine("<table>");
foreach (DataRow row in table.Rows) {
foreach (DataColumn column in table.Columns) {
sb.AppendFormat("<tr><td style='background-color:#113;'>{0}</td>" +
"<td>{1}</td></tr>",
column.ColumnName, row[column]);
}
sb.AppendFormat("<tr style='height:10px;'></tr>");
}
sb.AppendLine("</table>");
return sb.ToString();
} finally {
if (sf != null)
sf.Close();
}
}
And here’s the very simple Web Method that exposes the method’s functionality to client script:
[WebMethod]
public string GetPlaceInfo(string fileName, double latitude, double longitude) {
ShapefileReader sr = new ShapefileReader();
return sr.QueryPoint(Server.MapPath("~/Maps/" + fileName), latitude,
longitude);
}
Finally, this is what happens on the client when the user clicks on a point on the map. Notice how we leverage ASP.NET AJAX’s page lifecycle “pageLoad” event to initially setup our map and register the onclick event handler. The rest of this snippet should be self explanatory. In case it is not, you can have a look at Virtual Earth SDK to learn more about map events and methods.
function pageLoad() {
map = new VEMap('myMap');
map.LoadMap(new VELatLong(50.5, -2.1), 6);
map.AttachEvent("onclick", map_onclick);
}
function map_onclick(e) {
if (shapefileName != null) {
var ll = map.PixelToLatLong(new VEPixel(e.mapX, e.mapY));
MapService.GetPlaceInfo(shapefileName, ll.Latitude, ll.Longitude,
QueryData_success, onFailed);
}
}
function QueryData_success(e) {
if (e != null)
MessageBox(true, e, 0, true);
else
MessageBox(true, "No data to display", 2000);
}
That’s all folks! I hope you enjoyed this article and found it useful! As always, I urge you to leave a comment if you need any more info or clarification. Finally, if you liked this article and you want to help me spread the word, you might consider clicking on the “kick it” button below! 😉
As usual, you can see a live demo of the application here, and you can download the source code of this project here.
Enjoy!