Local Tile Layers in Bing Maps WPF

Earlier this year the Bing Maps WPF control was updated with a number of performance and bug fixes. Most notably, navigating the map using touch works much better in Windows 8 than it did before. This update also increases the number of supported cultures from 15 to 117 and aligns with the same cultures supported by the Bing Maps Windows Store SDK which are documented here. One new feature is the ability to have a lot more control over tile layers. In the past tiles for a tile layer had to be hosted on a server in order to load them into the WPF map control. Now you can extend the TileSource class and load in tiles from local storage or generate them right in code. In this blog post we are going to take a look at how we can create a custom tile source that loads tiles from a local folder.

Full source code for this project can be found on the MSDN Code Sample Gallery here.

Getting Started

To get started open up Visual Studio and create a new WPF application project called LocalTileLayer.WPF.

clip_image002

Screenshot: Creating the project in Visual Studio

Referencing the Bing Maps WPF control

There are two ways to add the Bing Maps WPF control to your project.

The first method is to download the SDK from here. Once this is done you can right click on your project name and select Add Reference. Next browse to the location where the Bing Maps WPF Control installed. Typically, the control is installed in the Program Files or Program Files (x86) folders on the drive that contains your operating system. Open the Libraries folder and select the Microsoft.Maps.MapControl.WPF.dll file and then click OK.

With the release of this update the Bing Maps team has made the Bing Maps WPF control available through NuGet as you can see here. To add this to your project in Visual Studio go to Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution. In the windows that appears, search for “Bing Maps WPF Control”.

clip_image003

Screenshot: Bing Maps WPF control in NuGet Package Manager

Select the NuGet package that was created by the Microsoft Bing Maps team and press the install button and select your project that want to have the package installed into.

Creating a custom Tile Source

Create a new class called LocalTileLayerSource. This class will extend from the TileSource class. When creating an instance of this class we will have the user specify the path to where the tiles are stored. When this class loads it will set the DirectImage property of the TileSource to point to a new ImageCallback method which we will call TileRender. Putting this together the class looks like this:

 using Microsoft.Maps.MapControl.WPF;
using Microsoft.Maps.MapControl.WPF.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace LocalTileLayer.WPF
{
    public class LocalTileSource : TileSource
    {
        private string _tilePath = string.Empty;

        public LocalTileLayerSource(string tilePath)
        {
            _tilePath = tilePath;

            this.DirectImage = new ImageCallback(TileRender);
        }

        public BitmapImage TileRender(long x, long y, int zoomLevel)
        {
            //Add code to create tile.
        }
}

The TileRender method takes in three properties; x, y, and zoomLevel. These properties define the position of the tile to render. Another common way of defining the position of a tile is using a quadkey value. A quadkey value is a base-4 number where the number of digits is equal to the zoom level of the tile, and the numbers specify the where on the map the tile is. The benefit of using quadkey values is that it is a single value that identifies a tile. Details on how quadkeys are documented here. Since both methods of identifying a tile are commonly used we will make sure the TileRender method handles both of these by replacing the relevant properties in the tile path string and then loading the image from the path. If the tile doesn’t exist or a tile path isn’t specified the TileRender method will need to return an empty image, otherwise it may try scaling a tile from a previous zoom level to fill in the gaps. Update the TileRender method with the following code:

 public BitmapImage TileRender(long x, long y, int zoomLevel)
{
    if (!string.IsNullOrWhiteSpace(_tilePath))
    {
        var q = new QuadKey((int)x, (int)y, zoomLevel);
                
        var filePath = _tilePath.Replace("{quadkey}", q.Key).Replace("{x}", x.ToString()).Replace("{y}", y.ToString()).Replace("{zoomlevel}", zoomLevel.ToString());

        var fi = new FileInfo(filePath);
        if (fi.Exists)
        {
            using (var fStream = fi.OpenRead())
            {
                var bmp = new BitmapImage();
                bmp.BeginInit();
                bmp.StreamSource = fStream;
                bmp.CacheOption = BitmapCacheOption.OnLoad;
                bmp.EndInit();
                return bmp;
            }
        }
    }

    return CreateTransparentTile();
}

Next we will add the CreateTransparentTile method. This method will return a transparent BitmapImage that is the same size as a standard tile. Add the following code to the LocalTileSource class.

 private BitmapImage CreateTransparentTile()
{
    int width = 256;
    int height = 256;

    var source = BitmapSource.Create(width, height,
                                    96, 96,
                                    PixelFormats.Indexed1,
                                    new BitmapPalette(new List<Color>(){
                                        Colors.Transparent
                                    }),
                                    new byte[width * height],
                                    width);

    var image = new BitmapImage();

    var memoryStream = new MemoryStream();

    var encoder = new PngBitmapEncoder();
    encoder.Frames.Add(BitmapFrame.Create(source));
    encoder.Save(memoryStream);

    memoryStream.Position = 0;
            
    image.BeginInit();
    image.StreamSource = memoryStream;
    image.EndInit();

    return image;
}

Implementing the custom TileSource

To get started we need to add a map to the app. Open the MainWindow.xaml file and update the XAML to the following:

 <Window x:Class="LocalTileLayer.WPF.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" Center="51.554,0.6877" ZoomLevel="15"
               CredentialsProvider="YOUR_BING_MAPS_KEY"/>
    </Grid>
</Window>

This will load Bing Maps to zoom level 15 over a Southend University Hospital in Essex, UK. I created a set of tiles from floor plan of the hospital which I found here using Microsoft’s MapCruncher tool. You can either create your own tile layer with MapCruncher or download the source code for this project and grab the tiles from there. Create a folder in the project called SouthendUniversityHospital and copy all the tiles to it. Select all the tile images in this folder, right click and go to Properties. Set the Build Action to Content and the Copy to Output Directory option to Copy if Newer.

Open the MainWindow.xaml.cs file. When the map loads we will add a tile layer to it that makes use of the LocalTileSource class we created and points to our tiles. Update the code to look like this:

 using Microsoft.Maps.MapControl.WPF;
using System.Windows;

namespace LocalTileLayer.WPF
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            MyMap.Loaded += MyMap_Loaded;
        }

        private void MyMap_Loaded(object sender, RoutedEventArgs e)
        {
            var tileLayer = new MapTileLayer()
            {
                TileSource = new LocalTileSource(@"SouthEndUniversityHospital\{quadkey}.png")
            };
            MyMap.Children.Add(tileLayer);
        }
    }
}

If you build and run the application you will see the map loads over a hospital in Essex, UK and renders a custom tile layer, that is hosted locally, over top the map.

clip_image005

Screenshot: Custom local tile layer displayed on map.

In this blog post we have seen how to you can extend the TileSource class in the Bing Maps WPF control so that it loads tiles from local storage. If you want to optimize this class further, you could also add a bounding box for the tile layer and zoom range and check to see if a requested tile falls within these values. If it doesn’t you could then go straight to returning an empty image and not have to check to see if there is an image file for the tile.

The TileSource class can also be easily be extended to programmatically generate tiles from data as well. If you had thousands of pushpins that you want to render on the map, it may be faster to programmatically render the pushpins on a tile layer. As the map would only need to keep track of a handful of tiles instead of thousands of pushpins.

As noted at the start of this blog, you can download the full source code for this blog from the MSDN Code Sample Gallery here.

- Ricky Brundritt, Bing Maps Senior Program manager

Related Blog Posts: