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.

clip_image002

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:

clip_image004

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.

clip_image006

Here is an example with county boundaries being rendered.

clip_image008

Getting Data

Here are a couple of good sources for ESRI Shapefiles to test with:

ogr2ogr -s_srs EPSG:27700 -t_srs EPSG:4326 outputFileInWGS84.shp inputFileInOSGB36.shp