Display Shapefiles in WPF
Shapefiles are a great way to store geospatial information: it's file-based, fairly compact, and portable. Increasingly, however, it has become harder and harder to create applications that allow you to use your data in the format of your choosing without having to first upload it to some other company's server and use their proprietary format.
In this article, we will cover how to create an application that shows you how easy it is to display shapefiles in WPF using ThinkGeo Desktop Maps. The data that will be used in this article is sourced from Natural Earth, which has an awesome collection of freely available shapefile data. Here is the application in action using their land data:
Project Setup
We'll start off by creating a new project in Visual Studio 2019. You can use the first few steps in Microsoft's guide to Create a WPF Application on their documentation website. Once the project is opened in Visual Studio, you are ready to move on to the next step.
Install NuGet Packages
In order to display our shapefile data on a map, we will use ThinkGeo's WPF NuGet package that will provide us with a map control and a full suite of GIS tools to use.
Open the NuGet Package Manager by right-clicking on your project in the Solution Explorer and select Manage NuGet Packages
. Search for ThinkGeo.UI.Wpf
and add it to your project.
Or if you prefer, you can install the package using the dotnet CLI:
dotnet add package ThinkGeo.UI.WPF
After the installation is complete, you'll see several packages installed. The most important of which being:
ThinkGeo.UI.WPF
- WPF controls for displaying maps, interactive spatial drawing, and more.ThinkGeo.Core
- GIS classes and functions for layers, styles, and performing geospatial operations.
NOTE
In order to use ThinkGeo products in your application, you will need to create a ThinkGeo Account and install the license on your development environment.
Follow ThinkGeo's guide to getting started with ThinkGeo Desktop Maps to install a free 30 day trial license.
Add the Map Control
Now that our application is setup, we can start building. We'll start with adding a map to the window. In order to use ThinkGeo's map control, we'll need to add the ThinkGeo namespace in the Window
control in MainWindow.xam
l:
xmlns:tg="clr-namespace:ThinkGeo.UI.Wpf;assembly=ThinkGeo.UI.Wpf"
With the namespace added, we can now add the MapView
control to the Grid
:
<Grid>
<!-- Map -->
<tg:MapView x:Name="map" MapUnit="DecimalDegree" Loaded="Map_Loaded"/>
</Grid>
We'll name the MapView control to "map" so that we can reference it in the code-behind. We also need to set the map's unit of measurement to match the measurement of the data. In this case, we are using data from Natural Earth, so we will use DecimalDegree as the map unit to match. Finally, a Map_Loaded event handler is added to the map so that we can load the data as soon as the map is ready.
NOTE
GIS data in different units of measurement from the map will have to be reprojected on the fly from its unit type to the map's unit type in order to be displayed properly. We'll cover the nuances of reprojection in a future blog article.
Display a Shapefile on the Map
With the MapView added to the application's window, we can begin to display the shapefile in the map control.
Create the Shapefile Layer
Download Natural Earth's land data, create a ShapeFileFeatureLayer in Map_Loaded method and style it.
private void Map_Loaded(object sender, RoutedEventArgs e)
{
// Create a shapefileLayer, passing in the path of a shape file.
ShapeFileFeatureLayer shapefileLayer = new ShapeFileFeatureLayer("Data/ne_110m_land.shp");
// Set the default area style and apply it to all 20 ZoomLevels
shapefileLayer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle = new AreaStyle(GeoPens.Black);
shapefileLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
}
As you can see, styles are added to what is called a ZoomLevelSet
. A ZoomLevelSet
, by default, contains 20 zoom levels, or geometric scale breakpoints, that allow you to design how your layer looks depending on how zoomed in the user is on the map. You can then assign multiple styles at each ZoomLevel
and apply those styles across other scales.
In our case, we are just assigning a simple AreaStyle
with a black outline on all 20 zoom levels.
Add the Layer to a LayerOverlay
Now that the layer is styled, we need to add a LayerOverlay
that will contain our shapefile layer. An Overlay
is essentially a container that holds a collection of objects. There are several types overlays that contain things such as map adornments, feature layers, and editable features. In this case, we need a LayerOverlay
to contain Layer
objects for our shapefile. We'll then add that overlay to the map so that it will be visible.
private void Map_Loaded(object sender, RoutedEventArgs e)
{
// Create a shapefileLayer, passing in the path of a shape file.
[...]
// Set the default area style and apply it to all 20 ZoomLevels
[...]
// Create an Overlay to store the layer and add it to the map.
LayerOverlay overlay = new LayerOverlay();
overlay.Layers.Add(shapefileLayer);
map.Overlays.Add(overlay);
}
With that, we should now be able to see our layer on the map. But depending on the data, it may be hard to see. So, in the next section, we will automatically adjust the map's view to show the entire shapefile when the application loads.
Set the Map's Extent
When the application loads, we want to make sure we can see the shapefile data. In order to do this, we can simply make a query on the shapefile to find the bounding box of all its data. We can then set the Map's view to match that:
private void Map_Loaded(object sender, RoutedEventArgs e)
{
// Create a shapefileLayer, passing in the path of a shape file.
[...]
// Set the default area style and apply it to all 20 ZoomLevels
[...]
// Add the layer to the ShapefileOverlay
[...]
// Set the map's extent to the bounding box of the shapefile
ShapeFileFeatureLayer.BuildIndexFile("Data/ne_110m_land.shp");
shapefileLayer.Open();
map.CurrentExtent = shapefileLayer.GetBoundingBox();
map.Refresh();
}
We did something a little sneaky here. Not all shapefiles come with an index file. Without this file, query and drawing performance drops dramatically. So, if our application doesn't find one, we'll just create one for them using the ShapeFileFeatureLayer.BuildIndexFile
static method. This can impact the initial load of the layer while the index is being created, but once created, subsequent loads will be many times faster.
Now that we have ensured an index exists, we can efficiently perform queries on the layer. We do this by first opening the layer. Here, we are using a very common query of GetBoundingBox
and setting the Map's CurrentExtent
property to it. Finally, we call Refresh
on the Map to indicate that we are ready for it to redraw.
With that, our application is complete. Here is the gitlab repo where you can find the complete source code.
Run the Application
When you run the application, you will see the shapefile displayed on the map. Below is how the application looks when I load up Natural Earth's basic land data:
Closing Remarks
Thank you for sticking with me through this tutorial. Be sure to check out our other education blog articles for more of what ThinkGeo can do for you. Let us know if there are any subjects that you would like to see covered in future blog articles!
Check out ThinkGeo's Desktop Maps product page for more information on what features you'll get from our products!