Tile Layers are a creative way to visualize large complex data. By visualizing data as an image the map control only needs to reposition a set of images rather than every single data point of the data. This greatly improves performance and often reduces the amount of data the user will have to download. Tile layers generally consist of a large number of images that have a specific naming convention and are hosted online. Every once and a while I get requests from people who want to be able to host the tile layers locally. Here is a list of some of the most common reasons for wanting to have local tile layers in apps that I have come across:
- Make the app self-contained and not have to host the tile online somewhere.
- Create an app where users can purchase additional tile layers that the developer created.
- Don’t want to make the tiles available online for security reasons.
Creating a local tile layer is not an easy task. If you have a development environment you could potentially host a local tile layer using IIS. However, Windows Store apps are sandboxed and won’t be able to access the localhost. If you attempt to store your tiles in your app and simply pass in a reference to the local tiles into the MapTileLayer class in Bing Maps you will also find this doesn’t work. The reason for this is that the MapTileLayer class is designed to only access map tiles that are hosted online. It is still possible to create local tile layers, in fact, I have done this with several of the other Bing Maps controls as well (WPF, Silverlight and WP7). I also created a version for Windows Store apps which is the basis of this blog post and can be found in the MSDN Code samples. In this blog post instead of going through the 900+ lines of code required to create a local tile layer I will describe the process required to create one and how to make use of the library I created in the code samples.
Tip: Tile layers can become large. If your tile layer images are more than a couple hundred megabytes then consider hosting them online rather than in the app. If your app takes up a lot of space there is a greater chance of a user deleting it to make space on their device.
The Bing Maps Tile System
Before we can get to work we need to understand how the Bing Maps tile system works. Bing Maps provides a world map that users can directly manipulate to pan and zoom. To make this interaction as fast and responsive as possible, the map is broken up into a large number of images at different levels of detail. These images are often referred to as tiles, as they are stitched together by the map control like a tiled surface to create a complete map.
To optimize the performance of map retrieval and display, the rendered map is cut into tiles of 256 x 256 pixels each. As the number of pixels differs at each level of detail (zoom level), so does the number of tiles:
map width = map height = 256 * 2zoom level
Each tile is given an XY index ranging from (0, 0) in the upper left to (2zoom level–1, 2zoom level–1) in the lower right. For example, at level 3 the tile index ranges from (0, 0) to (7, 7) as follows:
To optimize the indexing and storage of tiles, the two-dimensional tile XY indices are combined into one-dimensional strings called quadtree keys, or “quadkeys” for short. Each quadkey uniquely identifies a single tile at a particular zoom level. To convert a tile index into a quadkey, the bits of the X and Y components are interleaved, and the result is interpreted as a base-4 number (with leading zeroes maintained) and converted into a string. Quadkeys have several interesting properties. First, the length of a quadkey (the number of digits) is equal to the zoom level of the corresponding tile. Second, the quadkey of any tile starts with the quadkey of its parent tile (the containing tile at the previous level). As shown in the example below, tile 2 is the parent of tiles 20 through 23, and tile 13 is the parent of tiles 130 through 133.
The mathematics required to do calculations with the Bing Maps tile system is well documented here.
Creating Tiles with Map Cruncher
There are a number of different ways to create a tile layer. If you don’t already have one, you can easily create a tile layer from a large image using MapCruncher. MapCruncher is a Microsoft Research project that makes it easy to cross reference an image with a location on a map and then turn the image into a tile layer. Instructions on how to use MapCruncher can be found here. Once you have done this go to the output folder and locate the Layer_NewLayer folder that contains the map tiles. Compress this folder into a zip file. Tile layers usually contain a lot of tiles. It is usually easier to zip them up, as it makes it a lot easier to move the tiles and also makes it easier for Visual Studios later because it will only need to keep track of the zip file and not every single map tile. The tiles should be at the root of the zip file and will look something like this:
The tiles that are in the code sample for this blog were created using a satellite image of Hurricane Katrina from NASA and MapCruncher. Check out NASA’s Earth Observatory for some great images.
There should also be a file called MapCruncherMetadata.xml in the output folder. If you open this up you should see a property called MapRectangle, which is the bounding box of the tile layer. Take note of this as it can be used to limit the area in which we attempt to load map tiles in our app layer. You can also determine the min and max zoom levels that tiles are available for by looking at the RangeDescriptors.
Tip: The tiles generated by MapCruncher are PNGs and are not as compressed as they could be. I recommend passing the tiles through an image compression tool like PngCrush. This will reduce the file size of each tile by finding the optimum compression based on the content of the image.
Creating a Local Tile Layer
As I mentioned at the start of this blog post, creating local tile layers is not easy. The main reason for this is that we basically need to create our own MapTileLayer class that is capable of calculating all the tile quadkeys needed for the viewable map area and load the tiles accordingly. To load a single tile on the map and position it correctly, we can make use of the method covered in a previous blog post on adding Image Overlays with Bing Maps. In that blog post the pixel coordinates of the bounding box of an image are calculated as the map moves and used to position and scale the image. To create a local tile layer we need to do basically the same thing, but instead of a single image we will have a bunch of images.
Most people would consider the hard part to be related to calculating which tiles are in view and their relative bounding boxes. However, that is actually not that difficult since we have all those useful mathematical methods available in the Bing Maps tile system documentation that I referred to earlier. The hard part is getting the local tile layer to have good performance. When the map is moving we will need to constantly update the position of the tiles that we have rendered so far. If we try and calculate all the tiles in view and attempt to load in new tiles while the map is moving, the map will likely become slow after a lot of panning of zooming. To avoid this we can delay the loading of new tiles until the map has finished moving. In addition,we can track which tiles we have loaded on the map in a list. That way we don’t have to load them again.
In the code samples are two projects. The first project is a class library that contains the LocalTileLayerclass I created along with additional helper classes. The second project is a sample Windows Store App that shows how to implement the local tile layer. If you open up the code samples in Visual Studios the structure of the projects will look like this:
The main class in the LocalTileLayers project is the LocalTileLayer class. This class contains all the logic for calculating which tiles need to be loaded, loading them from a zip file and then positioning them on the map. When creating an instance of the LocalTileLayer class you will need to pass in a reference to the map along with a LocalTileSource object. The LocalTileSource class allows you to specify a bunch of different configuration settings for your tile layer. These settings include:
|ZipTilePath||Uri||The path to where the zip file containing the map tiles is located. Make sure that the file is set as content and copied to the output directory of the project. Reference the file using a Uri like so new Uri("ms-appx:///Assets/HurricaneKatrina.zip")|
|FileExentsion||string||The file extension to append to the tile file name. (i.e. png, jpg, jpeg). Tile name format should be "[quadkey].[file extenion]" (i.e. 012312.png)|
|Bounds||LocationRect||The bounding box of where the tile layer has tiles. The whole world is used by default. Providing a bounding box helps reduce the number of invalid tiles that are loaded/removed and thus improves performance.|
|MinZoomLevel||int||The lowest zoom level in which tiles are available. If the user zooms out any more than the tiles from this zoom level will be scaled. This value should be smaller than the MaxZoomLevel value. Default is 1.|
|MaxZoomLevel||int||The highest zoom level in which tiles are available. If the user zooms in closer than this zoom level the tiles from this zoom level will be scaled. This value should be larger than the MinZoomLevel value. Default is 19.|
If you open up the MainPage.xaml file in the LocalTileLayerSample project you will find that this is a simple application that contains nothing more than a map. The XAML looks like this:
If you open the MainPage.xaml.cs file you can see how a LocalTileLayer is added to the map once it is loaded. This code looks like this:
In this code we can see that I created a LocalTileSource and specified not only the path to the zip file of tiles but also included the min and max zoom levels and the bounding box of the tile layer. The zoom and bounding box information doesn’t need to be specified if you don’t have it, but if you do specify it, it does reduce the number of calculations and non-existing images that the layer tries to load. Also, the LocalTileLayer class will try to fill in the zoom levels outside the range I specified by scaling the tiles from the closest zoom level that tiles are available. When creating the LocalTileLayer we simply pass in a reference to the map and to the LocalTileSource. I’ve also specified an opacity to make the layer partially transparent and then added the layer as a child of the map.
If you run the sample app, you will see a tile layer of hurricane Katrina over the United States. As you pan and zoom you will see new tiles loaded, even if you zoom in pass zoom level 6 as it will simply scale the tiles accordingly.
Notice how the resolution of the hurricane image increases significantly as you zoom into the map. This is one of the benefits of using tile layers. The original image was a high resolution GeoTiff image that was over 100MB in size. Using MapCruncher, I turned that image into tiles easily. The average tile size is about 30kb which means they will load a lot faster than the original image and also have higher resolution as you zoom into the map.
How You Can Take Things Further
Take a look at the blog post I wrote on creating Heat Maps in the Native Bing Maps control. That blog shows how to generate a heat map image and overlay it on the map. A similar process could be used to generate local tiles right within the app. These tiles could be generated by cropping a larger image or by drawing raw data on a tile and then be stored in local storage.