Accessing a WMS Tile Server from Bing Maps v7

A recent question on the MSDN forums asked about how to create an overlay from a WMS tile server on Bing Maps. A short web search revealed that, although there are some existing articles on this topic, they’re either somewhat broken or very old.  So, I thought I’d write up how to do this using Bing Maps AJAX v7.0. If you want to see how to access a WMS server using the Bing Maps Silverlight control, please see my other post.

Bing Maps are created from sets of image tiles – each 256px wide by 256px high. Each tile is referred to by its quadkey – a unique string consisting of characters between 0 – 3, which describes the position and zoom level at which this tile should be placed.

The following tile, for example, has the quadkey 120200223312:

r120200223312

There’s an excellent MSDN article that explains the Bing Maps Tile System in more detail, so I won’t bother repeating it here.

WMS servers, in contrast, create map images of (potentially) variable height and width, covering the features in the geographic area specified by a supplied bounding box. The bounding box defines the minimum and maximum longitude and latitude extents of the image – in other words, the West, South, East, and North points of the image.

Since Bing Maps thinks in terms of quadkeys, and WMS servers in terms of bounding boxes, to add a WMS layer to Bing Maps we simply need to intercept any tile layer requests via an intermediary handler, convert the quadkey to the relevant WMS bounding box, and request and return the supplied image as a tile to Bing Maps.

To start with, let’s define a basic map, and add a tile layer to it. The source for the tile layer will point to an .ashx web handler, which we’ll use to intercept the tile requests and pass them on to the WMS server. The handler will be passed a single parameter, q, which is the quadkey of the requested tile.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
  <title>Displaying WMS Tile Layers on Bing Maps v7</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
  <script type="text/javascript">
    function GetMap() {
      // Create a default map
      var map = new Microsoft.Maps.Map(
        document.getElementById("mapDiv"),
        { credentials: "ENTERYOURBINGMAPSKEY",
          center: new Microsoft.Maps.Location(50.5, 10),
          mapTypeId: Microsoft.Maps.MapTypeId.birdseye,
          zoom: 6
        }
      );

      // Define the tile layer source
      var tileSource = new Microsoft.Maps.TileSource({ uriConstructor: 'WMSHandler.ashx?q={quadkey}' });

      // Construct the layer using the tile source
      var tilelayer = new Microsoft.Maps.TileLayer({ mercator: tileSource, opacity: 0.9 });

      // Push the tile layer to the map
      map.entities.push(tilelayer);
    }
  </script>
</head>
<body onload="GetMap();">
<div id='mapDiv' style="position:relative; width:600px; height:800px;"></div>
</body>
</html>

Now, we need to define the WMSHandler.ashx handler that will request an image from the WMS server for a given tile quadkey. For the following example, I’ve used a WMS server that displays various images of data of Germany. You can use any WMS server you want, so long as it supports bounding box coordinates supplied using EPSG:4326, and returns images in Spherical Mercator projection as used by Bing Maps. You can find a list of publicly-accessible WMS servers at http://www.ogc-services.net/
The maths required to convert from a quadkey to N/S/E/W coordinates is contained in the following QuadKeyToBBOX() function. For more information, check out the MSDN Bing Maps Tile System article mentioned earlier.

Having obtained the appropriate coordinates, these are substituted into the url template at the {0} placeholder following the BBOX parameter. The image is then requested from the WMS server and served back to the Bing Maps client.

Here’s the C# code for the tile source handler:

using System;
using System.Web;
using System.Drawing; // Bitmap
using System.Drawing.Imaging; // ImageFormat
using System.Net; // WebClient
using System.IO; // MemoryStream

public class WMSHandler : IHttpHandler
{
// Bing Maps tiles are always 256px x 256px
public const int TILE_HEIGHT = 256, TILE_WIDTH = 256;

// Returns the bounding box coordinates for a given tile quadkey
public string QuadKeyToBBox(string quadKey)
{
int zoom = quadKey.Length;
int x = 0, y = 0;

// Work out the x and y position of this tile
for (int i = zoom; i > 0; i--)
{
int mask = 1 << (i - 1);
switch (quadKey[zoom - i])
{
case '0':
break;
case '1':
x |= mask;
break;
case '2':
y |= mask;
break;
case '3':
x |= mask;
y |= mask;
break;
default:
throw new ArgumentException("Invalid QuadKey digit sequence.");
}
}

// From the grid position and zoom, work out the min and max Latitude / Longitude values of this tile
double W = (float)(x * TILE_WIDTH) * 360 / (float)(TILE_WIDTH * Math.Pow(2, zoom)) - 180;
double N = (float)Math.Asin((Math.Exp((0.5 - (y * TILE_HEIGHT) / (TILE_HEIGHT) / Math.Pow(2, zoom)) * 4 * Math.PI) - 1) / (Math.Exp((0.5 - (y * TILE_HEIGHT) / 256 / Math.Pow(2, zoom)) * 4 * Math.PI) + 1)) * 180 / (float)Math.PI;
double E = (float)((x + 1) * TILE_WIDTH) * 360 / (float)(TILE_WIDTH * Math.Pow(2, zoom)) - 180;
double S = (float)Math.Asin((Math.Exp((0.5 - ((y + 1) * TILE_HEIGHT) / (TILE_HEIGHT) / Math.Pow(2, zoom)) * 4 * Math.PI) - 1) / (Math.Exp((0.5 - ((y + 1) * TILE_HEIGHT) / 256 / Math.Pow(2, zoom)) * 4 * Math.PI) + 1)) * 180 / (float)Math.PI;
double[] bounds = new double[] { W, S, E, N };

// Return a comma-separated string of the bounding coordinates
return string.Join(",", Array.ConvertAll(bounds, s => s.ToString()));
}

public void ProcessRequest(HttpContext context)
{
// Retrieve the requested quadkey
string quadKey = context.Request.QueryString["q"];
string bbstr = QuadKeyToBBox(quadKey);

// Define the URL of the WMS service, with placeholder for bounding box
string urlTemplate = "http://wms1.ccgis.de/cgi-bin/mapserv?map=/data/umn/germany/germany.map&&VERSION=1.1.1&REQUEST=GetMap&SERVICE=WMS&SRS=EPSG%3A4326&BBOX={0}&WIDTH=256&HEIGHT=256&LAYERS=Topographie";

// Insert the bounding box coordinates into the URL
string url = string.Format(urlTemplate, bbstr);

// Retrieve the tile image from the WMS server
WebClient webClient = new WebClient();
byte[] bImage = webClient.DownloadData(url);
MemoryStream memoryStream = new MemoryStream(bImage);
Bitmap wmsBitmap = (Bitmap)Bitmap.FromStream(memoryStream);
memoryStream.Close();
// Convert image to stream
MemoryStream streamImage = new MemoryStream();
wmsBitmap.Save(streamImage, ImageFormat.Png);
wmsBitmap.Dispose();

// Send response back to the client
context.Response.ContentType = "image/png";
context.Response.AddHeader("content-length", System.Convert.ToString(streamImage.Length));
context.Response.BinaryWrite(streamImage.ToArray());

// Clean up
streamImage.Dispose();
}

public bool IsReusable
{
get
{
return false;
}
}
}

And here’s what it looks like when you view this WMS layer on Bing Maps, illustrating the topography of Germany:

image

using System;
using System.Web;
using System.Drawing; // Bitmap
using System.Drawing.Imaging; // ImageFormat
using System.Net; // WebClient
using System.IO; // MemoryStream 

public class WMSHandler : IHttpHandler
{
// Bing Maps tiles are always 256px x 256px
public const int TILE_HEIGHT = 256, TILE_WIDTH = 256;

// Returns the bounding box coordinates for a given tile quadkey
public string QuadKeyToBBox(string quadKey)
{
int zoom = quadKey.Length;
int x = 0, y = 0;

// Work out the x and y position of this tile
for (int i = zoom; i > 0; i–)
{
int mask = 1 << (i – 1);
switch (quadKey[zoom – i])
{
case ‘0’:
break;
case ‘1’:
x |= mask;
break;
case ‘2’:
y |= mask;
break;
case ‘3’:
x |= mask;
y |= mask;
break;
default:
throw new ArgumentException(“Invalid QuadKey digit sequence.”);
}
}

// From the grid position and zoom, work out the min and max Latitude / Longitude values of this tile
double W = (float)(x * TILE_WIDTH) * 360 / (float)(TILE_WIDTH * Math.Pow(2, zoom)) – 180;
double N = (float)Math.Asin((Math.Exp((0.5 – (y * TILE_HEIGHT) / (TILE_HEIGHT) / Math.Pow(2, zoom)) * 4 * Math.PI) – 1) / (Math.Exp((0.5 – (y * TILE_HEIGHT) / 256 / Math.Pow(2, zoom)) * 4 * Math.PI) + 1)) * 180 / (float)Math.PI;
double E = (float)((x + 1) * TILE_WIDTH) * 360 / (float)(TILE_WIDTH * Math.Pow(2, zoom)) – 180;
double S = (float)Math.Asin((Math.Exp((0.5 – ((y + 1) * TILE_HEIGHT) / (TILE_HEIGHT) / Math.Pow(2, zoom)) * 4 * Math.PI) – 1) / (Math.Exp((0.5 – ((y + 1) * TILE_HEIGHT) / 256 / Math.Pow(2, zoom)) * 4 * Math.PI) + 1)) * 180 / (float)Math.PI;
double[] bounds = new double[] { W, S, E, N };

// Return a comma-separated string of the bounding coordinates
return string.Join(“,”, Array.ConvertAll(bounds, s => s.ToString()));
}

public void ProcessRequest(HttpContext context)
{
// Retrieve the requested quadkey
string quadKey = context.Request.QueryString[“q”];
string bbstr = QuadKeyToBBox(quadKey);

// Define the URL of the WMS service, with placeholder for bounding box
string urlTemplate = “http://wms1.ccgis.de/cgi-bin/mapserv?map=/data/umn/germany/germany.map&&VERSION=1.1.1&REQUEST=GetMap&SERVICE=WMS&SRS=EPSG%3A4326&BBOX={0}&WIDTH=256&HEIGHT=256&LAYERS=Topographie”;

// Insert the bounding box coordinates into the URL
string url = string.Format(urlTemplate, bbstr);

// Retrieve the tile image from the WMS server
WebClient webClient = new WebClient();
byte[] bImage = webClient.DownloadData(url);
MemoryStream memoryStream = new MemoryStream(bImage);
Bitmap wmsBitmap = (Bitmap)Bitmap.FromStream(memoryStream);
memoryStream.Close();
// Convert image to stream
MemoryStream streamImage = new MemoryStream();
wmsBitmap.Save(streamImage, ImageFormat.Png);
wmsBitmap.Dispose();

// Send response back to the client
context.Response.ContentType = “image/png”;
context.Response.AddHeader(“content-length”, System.Convert.ToString(streamImage.Length));
context.Response.BinaryWrite(streamImage.ToArray());

// Clean up
streamImage.Dispose();
}

public bool IsReusable
{
get
{
return false;
}
}
}

This entry was posted in Bing Maps and tagged , . Bookmark the permalink.

7 Responses to Accessing a WMS Tile Server from Bing Maps v7

  1. Chris says:

    Really enjoy your blog posts, and especially like this one, thanks for sharing it.

    One idea I had, is you could improve the loading performance of this layer, by saving the png as a transparent 8 bit png. NOAA uses a 256 color pallet, which should be accomodated with 8bits

  2. Pingback: The 4th Dimension – Creating Dynamic Animated Tile Layers in Bing Maps (AJAX v7) – Part 4 | Alastair Aitchison

  3. Great alastaira! … the code worked as how you have shown it. very comprehensive!

  4. Pat says:

    This will work using EPSG:3857 (Pretty Mercator) which uses spherical maps
    For EPSG:4326 which uses an elliptical earth you will get a distorted image for many larg scale maps (e.g. level 1, 2, 3, 4, 5) the more you zoom in and the more you are zooming at the equator the map will be more correct.

  5. Andy says:

    Has anyone done this using PHP for the tile source handler, instead of C#? I’ve Googled around and can only find references to doing it in C#, which won’t work for our application.

  6. John says:

    I don’t suppose anyone could provide an example using Java as a tile source handler. ???

Leave a comment