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:
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:
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;
}
}
}
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
Thanks for the comment Chris, and I’m really glad you enjoy reading my blog! Very good point about reducing unnecessary colour depth in the PNG files – see https://alastaira.wordpress.com/2011/05/29/the-4th-dimension-creating-dynamic-animated-tile-layers-in-bing-maps-ajax-v7-part-4/
Pingback: The 4th Dimension – Creating Dynamic Animated Tile Layers in Bing Maps (AJAX v7) – Part 4 | Alastair Aitchison
Great alastaira! … the code worked as how you have shown it. very comprehensive!
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.
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.
I don’t suppose anyone could provide an example using Java as a tile source handler. ???