How to create bounded tile layers in Bing Maps (JavaScript)

Tile layers are a great way to visualize a lot of data on a map and still have good performance. Depending on the type of data you are displaying as a tile layer, you may find that it is localized to a small part of the world. If this is the case then you will want to limit the loading of tiles in your app to that area. Otherwise you will end up with a lot of extra requests being made to your tile service for nothing. In this blog post, I will show you how to create bounded tile layers in Bing Maps Web Control  and the Bing Maps for Windows Store Apps control.

Bing Maps supports tile layers in all of its map controls and has done so for a long time. Many of these controls provide methods for limiting tile layers to a specific bounding box or zoom level range. In the Bing Maps Web Control Version and Bing Maps for Windows Store Apps JavaScript map controls tile layers are designed to be much more customizable. Instead of providing properties for restricting the region the tile layer renders in, it gives us the ability to provide a callback function where we can add advanced functionality for loading tile layers. In this blog post we are going to see how we can create a custom tile layer in Bing Maps Web Control Version 7 and the Bing Maps for Windows Store Apps JavaScript map controls that is limited to a bounding box area and a zoom level range. In addition, we will also make it so that this tile layer can support tile services that use quadkey, x y and zoom, or bounding box naming conventions for retrieving tiles. In this blog post we will cover how to create a custom tile layer in Bing Maps Web Control  and the Bing Maps for Windows Store Apps JavaScript map controls that is limited to a bounding box area and a zoom level range. Also, we will make it so that this tile layer can support tile services that use quadkey, x y and zoom, or bounding box naming conventions for retrieving tiles.

Helpful samples for using the Bing Maps Web control can be found at Samples.BingMapsPortal.com and in the Interactive SDK

Setting up the base project

In this blog post you have the choice of creating a web or a Windows Store app. The web based app will make use of the Bing Maps AJAX Control Version 7 while the Windows Store app will make use of the Bing Maps for Windows Store Apps SDK. Both APIs are nearly identical, but there are a couple of minor differences. The main difference is that we will be using different JavaScript references to load the map control into the page. The Windows Store app will reference a local copy of the Bing Maps API while the web app will be loading in a cloud hosted version of the Bing Maps API. Since the APIs are nearly identical, the only differences between the Windows Store and web app will be the main HTML page that loads in the script references needed for the app.

Creating a web based project

If you would like to create a web based app open Visual Studio and create a new ASP.NET Web Application called BoundedTileLayer.

Screenshot: Creating a new ASP.NET Web Application

Screenshot: Creating a new ASP.NET Web Application

In the window that opens, select the Empty template. We will be creating a basic web app to keep things simple. Next create a new HTML page called default.html by right clicking on the project and selecting Add → New Item. Create a folder called js and in that folder create two JavaScript files called default.js and BoundedTileLayer.js.

Creating a Windows Store project

If you would like to create a Windows Store app then create a new JavaScript Windows Store project. Select the Blank Template and call the project BoundedTileLayer.Win8.

Screenshot: Creating a new JavaScript Windows Store project

Screenshot: Creating a new JavaScript Windows Store project

Add a reference to the Bing Maps for Windows Store Apps SDK. To do this, right click on the References folder and press Add Reference. Select WindowsExtensions and then select Bing Maps for JavaScript. If you do not see this option ensure that you have installed the Bing Maps for Windows Store Apps SDK.

Screenshot: Adding a reference

Screenshot: Adding a reference

Next, right click the js folder and select Add  New Item. Create a new JavaScript file called BoundedTileLayer.js.

Setting up the UI

The JavaScript we will use in this app will be identical for both the web and Windows Store app. The defaut.html file, however, will have some differences. The main difference will be with the JavaScript script files being referenced in the header. The page itself will consist of a map and a couple of buttons for adding tile layers.

If you are creating a web based application update the default.html file with the following HTML:

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <!-- Add Script references to Bing Maps SDK -->
    <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
   1:

   2:

   3:     <!-- Add Script references to our JS files -->

   4:     <script type="text/javascript" src="js/default.js">

   1: </script>

   2:     <script type="text/javascript" src="js/BoundedTileLayer.js">
</script>

<style>
html, body {
height: 100%;
width: 100%;
padding: 0px;
margin: 0px;
}
</style>
</head>
<body>
<div id='myMap' style='position:relative;width:100%;height:100%;'></div>

<div style="position:absolute;top:5px;right:10px;">
<input id="XyzTileBtn" type="button" value="Load XYZ Tile Layer" />
<input id="WmsTileBtn" type="button" value="Load WMS Tile Layer" />
</div>
</body>
</html>

 

If you are creating a Windows Store app update the default.html file with the following HTML:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.2.0/css/ui-dark.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.2.0/js/base.js"></script>
   1:

   2:     <script src="//Microsoft.WinJS.2.0/js/ui.js">

   1: </script>

   2:

   3:     <!-- Add Script references to Bing Maps SDK -->

   4:     <script type="text/javascript" src="ms-appx:///Bing.Maps.JavaScript//js/veapicore.js">

   1: </script>

   2:

   3:     <!-- BoundedTileLayer.Win8 references -->

   4:     <link href="/css/default.css" rel="stylesheet" />

   5:     <script src="/js/default.js">

   1: </script>

   2:     <script src="/js/BoundedTileLayer.js">
</script>
</head>
<body>
<div id='myMap'></div>

<div style="position:absolute;top:5px;right:10px;background-color:#000;padding:5px;">
<input id="XyzTileBtn" type="button" value="Load XYZ Tile Layer" />
<input id="WmsTileBtn" type="button" value="Load WMS Tile Layer" />
</div>
</body>
</html>

 

If you try to run the application at this point you won’t see too much, as the map hasn’t been loaded. We will add the code to load the map to the default.js file. Also, we will include a couple of event handlers for the buttons. Add the following code to the default.js file:

(function () {
    function initialize() {
        //Check to see if Windows Store version of Bing Maps is being used
        if (Microsoft.Maps.ClientRegion) {
            Microsoft.Maps.loadModule('Microsoft.Maps.Map', { callback: GetMap });
        } else {
            GetMap();
        }
    }

    var map;

    function GetMap() {
        map = new Microsoft.Maps.Map(document.getElementById("myMap"), {
            credentials: "YOUR_BING_MAPS_KEY"
        });

        //Click events for test buttons
        document.getElementById('XyzTileBtn').addEventListener('click', AddTileXYZLayer);
        document.getElementById('WmsTileBtn').addEventListener('click', AddWMSLayer);
    }

    function AddTileXYZLayer() {
    }

    function AddWMSLayer() {
    }

    document.addEventListener("DOMContentLoaded", initialize, false);
})();

 

If you run the application you will see the map load up and a couple of buttons appearing in the top right corner of the map like this:

Example: Map with load tile buttons

Example: Map with load tile buttons

Creating a bounded tile layer class

The goal of this blog post is to create a tile layer class that can be bounded to a bounding box region and limited to a specified zoom level range. By doing this we can limit the request for tiles to only the zoom levels and area in which we have tiles. Thiscan significantly reduce the number of requests being made to your tile service. While we are at it, we are going to add support for several different tile layer services. Some tile layers, like the base maps in Bing Maps, use a quadkey tile naming format, while others use an X, Y and zoom level value. Web Mapping Services (WMS) use bounding box coordinates.

At the begining of this blog I mentioned that tile layers in the Bing Maps AJAX Control Version 7 and Bing Maps for Windows Store Apps map controls allow us to provide a callback function for doing more advanced things with tile layers. This callback function receives an object that has three properties; x, y and levelOfDetail (zoom level). If we want our bounded tile layer class to support quadkey tile layers, we will need to calculate the quadkey value from this information. This is actually well documented already in the MSDN documentation here. To be able to limit a tile layer to a bounding box, we need to know the bounding box of a tile. We also need this information to support WMS tile layers. I’ve done this calculation a number of times before in previous blog posts and will reuse that code to create a function called getTileBounds. To get started with our bounded tile layer class, open the BounedTileLayer.js file and add the following two functions:

function tileXYZoomToQuadkey(tile){
    var quadKey = [];

    for (var i = tile.levelOfDetail; i > 0; i--) {
        var digit = '0';
        var mask = 1 << (i - 1);

        if ((tile.x & mask) != 0) {
            digit++;
        }

        if ((tile.y & mask) != 0) {
            digit++;
            digit++;
        }

        quadKey.push(digit);
    }

    return quadKey.join('');
}

function getTileBounds(tile) {
    var mapSize = Math.pow(2, tile.levelOfDetail);

    var west = ((tile.x * 360) / mapSize) - 180;
    var east = (((tile.x + 1) * 360) / mapSize) - 180;

    var efactor = Math.exp((0.5 - tile.y / mapSize) * 4 * Math.PI);
    var north = (Math.asin((efactor - 1) / (efactor + 1))) * (180 / Math.PI);

    efactor = Math.exp((0.5 - (tile.y + 1) / mapSize) * 4 * Math.PI);
    var south = (Math.asin((efactor - 1) / (efactor + 1))) * (180 / Math.PI);

    return Microsoft.Maps.LocationRect.fromEdges(north, west, south, east);
}

 

To create our BoundedTileLayer class we will have it take in a formatted URL and a set of standard Bing Maps tile layer options. We will also add support for three additional option properties: bounds, minZoom, maxZoom. We will then have this class wrap the standard tile layer class. However,instead of setting the uriConstructor property of the TileSource to the URL, we will give it a callback function. In this callback function we will calculate the bounding box of the tile and then check to see if it intersects with the specified bounding box for the tile layer. At the same time we will also check that the zoom level is within the specified zoom level range for the tile layer. If the tile does not meet these restrictions, we will have the callback return null. If the tile should be rendered, we will then calculate the quadkey value and then search and replace the different possible placeholders in the formatted tile layer URL. This class will support the following value placeholders: {quadkey}, {x}, {y}, {zoom}, {north}, {south}, {east}, and {west}. To do all this add the following code to the BoundedTileLayer.js file:

var BoundedTileLayer = function (sourceUrl, options) {
    if (sourceUrl == null || sourceUrl == '') {
        return null;
    }

    options = options || {};

    var bounds = options.bounds || Microsoft.Maps.LocationRect.fromEdges(85.5, -180, -85.5, 180);
    var minZoom = options.minZoom || 1;
    var maxZoom = options.maxZoom || 21;

    return new Microsoft.Maps.TileLayer({
        mercator: new Microsoft.Maps.TileSource({uriConstructor: function (tile) {
            var tileBounds = getTileBounds(tile);

            if (minZoom <= tile.levelOfDetail &&
                maxZoom >= tile.levelOfDetail &&
                tileBounds.intersects(bounds)) {

                var quadkey = tileXYZoomToQuadkey(tile);
                return sourceUrl.replace('{quadkey}', quadkey).replace('{x}', tile.x).
                    replace('{y}', tile.y).replace('{zoom}', tile.levelOfDetail).
                    replace('{north}', tileBounds.getNorth()).replace('{south}', tileBounds.getSouth()).
                    replace('{east}', tileBounds.getEast()).replace('{west}', tileBounds.getWest());
            }

            return null;
        }})
    });
};

 

Implementing the bounding tile layer class

Now that we have a bounded tile layer class we can implement it. For the first test we will add a tile layer that uses the x, y zoom level naming convention. The tile layer we will use for testing displays hiking trails all around the world and has a formatted tile URL like this:

http://tile.waymarkedtrails.org/hiking/{zoom}/{x}/{y}.png

To test our bounding class out we will limit the tile layer so that it only renders tiles over the United Kingdom, and between zoom levels 5 and 10. To do this, open the default.js file and update the AddTileXYZLayer function with the following code:

function AddTileXYZLayer() {
    map.entities.clear();

    var north = 60, south = 50, east = 1.73, west = -7.5;

    var xyZoomTileLayer = new BoundedTileLayer('http://tile.waymarkedtrails.org/hiking/{zoom}/{x}/{y}.png', {
        bounds: Microsoft.Maps.LocationRect.fromEdges(north, west, south, east),
        minZoom: 5,
        maxZoom: 10,
        opacity: .7
    });

    map.entities.push(xyZoomTileLayer);

    map.setView({ center: new Microsoft.Maps.Location(53.688, -2.039), zoom: 5 });
}

 

If you run the application and press the “Load XYZ Tile Layer” button, you will see a map that looks like the following image. Note that the reason why we are seeing data displayed over Germany when zoomed out is that a single tile is 256 pixels wide and the tiles contain data for the United Kingdom will likely also contain data for other regions when at higher zoom levels. As you zoom in you will see that the tiles will only load over the United Kingdom. If you watch the network traffic and pan the map so that that United Kingdom is out of view you will notice that there are no requests being made to the tile server, which is our main goal.

The second test will use a WMS tile service from NASA’s active fire in the last 48 hours WMS service. For this tile layer we will limit it to the mainland United States, and zoom levels 4 through 10. To implement this tile service update the AddWMSLayer function with the following code:

function AddWMSLayer() {
    map.entities.clear();

    var north = 49.234, south = 24.175, east = -65.573, west = -125.778;

    var wmsTileLayer = new BoundedTileLayer('https://firms.modaps.eosdis.nasa.gov/wms/?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=fires48&width=256&height=256&BBOX={west},{south},{east},{north}', {
        bounds: Microsoft.Maps.LocationRect.fromEdges(north, west, south, east),
        minZoom: 4,
        maxZoom: 10,
        opacity: .7
    });

    map.entities.push(wmsTileLayer);

    map.setView({ center: new Microsoft.Maps.Location(39.906, -99.018), zoom: 4 });
}

 

If you run the application and press the “Load WMS Tile Layer” button you will see a lot of red areas appear on the map where there have been fires in the United States. Again, since this tile layer contains data globally, we will some data in the surrounding area of the United States when at higher zoom levels. Below is a screenshot of what this layer looks like.

As mentioned at the beginning of this blog post the sample source code can be found in the Samples.BingMapsPortal.com site

– Ricky Brundritt, Bing Maps Program Manager