ESRI Shapefiles and Bing Maps WPF
In one of my previous blog posts I talked about several different ways of overlaying ESRI shapefile data onto Bing Maps. In this blog post I will walk through how to develop a simple application for loading a locally store shapefile onto the Bing Maps WPF control using the ESRI Shapefile ReaderCodePlex project.
Creating the project
First you will need to download the ESRI Shapefile Reader project. Once this is done you can unzip it and run the Visual Studio project. You will notice there are two libraries in the project. The first is called Catfood.Shapefile, this is the main library that has the logic for reading and parsing Shapefiles. The second is just a basic application for reading some metadata from a shapefile. We are only interested in the first project.
Open up Visual Studios and create a new WPF application call BingMapsShapefileViewer. Next right click on the solution and add an Existing project. Locate and add the Catfood.Shapefile project. Next right click on the References folder of the BingMapsShapefileViewer project and add a reference to the Catfood.Shapefile project. Your solution should look like this.
Adding the Map
Adding a map to the WPF control is pretty straight forward. You first need to add a reference to the WPF Map control library (Microsoft.Maps.MapControl.WPF.dll) which is usually located in the C:\Program Files (x86)\Bing Maps WPF Control\V1\Libraries directory.
Now that you have a reference to the map control in your project we can add a map in the xaml of the MainWindow.xaml file. While we are at it we will also add a MapLayer for the shapefile data as a child of the map control. We will also add two buttons, one to load in a shapefile and another to clear the map. Your xaml should look like this:
<Window x:Class="BingMapsShapefileViewer.MainWindow"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF"
Title="MainWindow" Height="350" Width="525">
<Grid>
<m:Map Name="MyMap" CredentialsProvider="YOUR_BING_MAPS_KEY">
<m:Map.Children>
<m:MapLayer Name="ShapeLayer"/>
</m:Map.Children>
</m:Map>
<StackPanel HorizontalAlignment="Right">
<Button Content="Load Shapefile" Click="LoadShapefile_Clicked"/>
<Button Content="Clear Map" Click="ClearMap_Clicked"/>
</StackPanel>
</Grid>
</Window>
If you build the project you should see a map with two buttons on it like this:
Reading a Shapefile
Now that we have our base application created we can add the logic to read in a shapefile when a user clicks the “Load Shapefile” button. To keep things easy we will only support shapefiles that are in the proper projection, WGS84 as re-projecting the data would make this much more complex and best covered in a future blog post.
We will use an OpenFileDialog to allow us to easily select the shapefile we wish to load to the map. Once a file is selected we can pass the file path into the Shapefile class from our Shapefile Reader library. Your code will look like this.
private void LoadShapefile_Clicked(object sender, RoutedEventArgs e)
{
ShapeLayer.Children.Clear();
OpenFileDialog dialog = new OpenFileDialog();
dialog.Title = "Select an ESRI Shapefile";
dialog.Filter = "ESRI Shapefile (*.shp) |*.shp;";
bool? valid = dialog.ShowDialog();
if (valid.HasValue && valid.Value)
{
using (Shapefile shapefile = new Shapefile(dialog.FileName))
{
//Logic to Process Shapefile
}
}
}
You can now loop through each shape in the shapefile and convert each shape in the shapefile into a Bing Maps shape and then add it to the map. Putting all this together we end up with the complete source code for the MainWindow.xaml.cs file.
using System.Windows;
using System.Windows.Media;
using Catfood.Shapefile;
using Microsoft.Maps.MapControl.WPF;
using Microsoft.Win32;
namespace BingMapsShapefileViewer
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
#region Constructor
public MainWindow()
{
InitializeComponent();
}
#endregion
#region Private Methods
private void LoadShapefile_Clicked(object sender, RoutedEventArgs e)
{
ShapeLayer.Children.Clear();
OpenFileDialog dialog = new OpenFileDialog();
dialog.Title = "Select an ESRI Shapefile";
dialog.Filter = "ESRI Shapefile (*.shp) |*.shp;";
bool? valid = dialog.ShowDialog();
if (valid.HasValue && valid.Value)
{
using (Shapefile shapefile = new Shapefile(dialog.FileName))
{
//Set the map view for the data set
MyMap.SetView(RectangleDToLocationRect(shapefile.BoundingBox));
foreach (Shape s in shapefile)
{
RenderShapeOnLayer(s, ShapeLayer);
}
}
}
}
private void ClearMap_Clicked(object sender, RoutedEventArgs e)
{
ShapeLayer.Children.Clear();
}
#region Helper Methods
private LocationRect RectangleDToLocationRect(RectangleD bBox)
{
return new LocationRect(bBox.Top, bBox.Left, bBox.Bottom, bBox.Right);
}
private void RenderShapeOnLayer(Shape shape, MapLayer layer)
{
switch (shape.Type)
{
case ShapeType.Point:
ShapePoint point = shape as ShapePoint;
layer.Children.Add(new Pushpin()
{
Location = new Location(point.Point.Y, point.Point.X)
});
break;
case ShapeType.PolyLine:
ShapePolyLine polyline = shape as ShapePolyLine;
for (int i = 0; i < polyline.Parts.Count; i++)
{
layer.Children.Add(new MapPolyline()
{
Locations = PointDArrayToLocationCollection(polyline.Parts[i]),
Stroke = new SolidColorBrush(Color.FromArgb(150, 255, 0, 0))
});
}
break;
case ShapeType.Polygon:
ShapePolygon polygon = shape as ShapePolygon;
if (polygon.Parts.Count > 0)
{
//Only render the exterior ring of polygons for now.
for (int i = 0; i < polygon.Parts.Count; i++)
{
//Note that the exterior rings in a ShapePolygon have a Clockwise order
if (!IsCCW(polygon.Parts[i]))
{
layer.Children.Add(new MapPolygon()
{
Locations = PointDArrayToLocationCollection(polygon.Parts[i]),
Fill = new SolidColorBrush(Color.FromArgb(150, 0, 0, 255)),
Stroke = new SolidColorBrush(Color.FromArgb(150, 255, 0, 0))
});
}
}
}
break;
case ShapeType.MultiPoint:
ShapeMultiPoint multiPoint = shape as ShapeMultiPoint;
for (int i = 0; i < multiPoint.Points.Length; i++)
{
layer.Children.Add(new Pushpin()
{
Location = new Location(multiPoint.Points[i].Y, multiPoint.Points[i].X)
});
}
break;
default:
break;
}
}
private LocationCollection PointDArrayToLocationCollection(PointD[] points)
{
LocationCollection locations = new LocationCollection();
int numPoints = points.Length;
for (int i = 0; i < numPoints; i++)
{
locations.Add(new Location(points[i].Y, points[i].X));
}
return locations;
}
/// <summary>
/// Determines if the coordinates in an array are in a counter clockwise order.
/// </summary>
/// <returns>A boolean indicating if the coordinates are in a counter clockwise order</returns>
public bool IsCCW(PointD[] points)
{
int count = points.Length;
PointD coordinate = points[0];
int index1 = 0;
for (int i = 1; i < count; i++)
{
PointD coordinate2 = points[i];
if (coordinate2.Y > coordinate.Y)
{
coordinate = coordinate2;
index1 = i;
}
}
int num4 = index1 - 1;
if (num4 < 0)
{
num4 = count - 2;
}
int num5 = index1 + 1;
if (num5 >= count)
{
num5 = 1;
}
PointD coordinate3 = points[num4];
PointD coordinate4 = points[num5];
double num6 = ((coordinate4.X - coordinate.X) * (coordinate3.Y - coordinate.Y)) -
((coordinate4.Y - coordinate.Y) * (coordinate3.X - coordinate.X));
if (num6 == 0.0)
{
return (coordinate3.X > coordinate4.X);
}
return (num6 > 0.0);
}
#endregion
#endregion
}
}
Here are a couple screen shots of this application loading in some different shapefiles. This first screen shot is all the interstate highways in the USA.
Here is an example with county boundaries being rendered.
Getting Data
Here are a couple of good sources for ESRI Shapefiles to test with:
- Zillow Neighborhood Boundaries
- DIVA-GIS – World Boundary data
- OS OpenData - Note this data is in OSGB36 projection and need to be re-projected before being used with Bing Maps. See this blog post for more information. These can easily be re-projected using a free tool is called the Geospatial Data Abstraction Library. Here is a quick example of the command needed for converting these files to WGs84 projection:
ogr2ogr -s_srs EPSG:27700 -t_srs EPSG:4326 outputFileInWGS84.shp inputFileInOSGB36.shp |
Comments
- Anonymous
March 12, 2014
For fast responses to questions try using the Bing Maps forums: social.msdn.microsoft.com/.../home If you are creating a Windows Store app then take a look at my free eBook: rbrundritt.wordpress.com/my-book