Silverlight and Virtual Earth are made for each other, but how best do we go about creating a Virtual Earth control in Silverlight? First up lets look at utilising the existing Deep Zoom control to serve up the base image tiles. In this proof of concept we have little more then a stock MultiScaleImage control hooked up to some appropriately created configuration XML, the output is only 6.8KB for the client! And check out the results on video:
Lets see how we do this with some proxy handler code.
Unfortunately I was unsuccessful in getting Silverlight Deep Zoom to request the tiles (images) directly from the Virtual Earth servers. I must note here that accessing the Virtual Earth tile servers directly is currently not allowed without permission, we hope this will change to allow for Silverlight applications very soon.
So first up I have the standard Deep Zoom code as produced from Deep Zoom Composer. I won’t bore you with that code. What I have done is created two xml files as the source for the multiscaleimage control. The first “VE.xml”” defines our layers, here I’ll do just one.
<?xml version="1.0" encoding="UTF-8"?> <Collection MaxLevel="8" TileSize="256" Format="jpg" Quality="1" NextItemId="1" xmlns="http://schemas.microsoft.com/deepzoom/2008"> <Items> <I Id="0" N="0" IsPath="1" Source="Tiles/VEAerial.xml"> <Size Width="1024" Height="1024" /> <Viewport Width="1" X="0" Y="0" /> </I> </Items> </Collection>
Then I’ll make one for a set of Virtual Earth tiles, “VEAerial.xml”:
<?xml version="1.0" encoding="UTF-8"?> <Image TileSize="256" Overlap="1" Format="jpg" xmlns="http://schemas.microsoft.com/deepzoom/2008"> <Size Width="134217728" Height="134217728"/> </Image>
The ability to define this in plain xml is new to Silverlight Beta2. I have observed that these files need to be saved as utf8, I had to use notepad to do this properly. Also you don’t change the “MaxLevel” attribute rather set a total size in pixels for the entire Earth to choose your maximum Virtual Earth zoom level, in this case that is a 134,217,728 pixel square image. Yes Deep Zoom seems happy with a 18 PetaPixel image!
Now I had hoped (and still do) that the mention from Scott Guthrie of “extensible MultiScaleTileSource support for DeepZoom (which allows developers to hook up existing image pyramids that don’t conform with the Deep Zoom format to the high performance rendering of Deep Zoom)” would allow some sort of native hook up to the VE tiles but I’m yet to find any more information.
Instead I decided to create a proxy handler that would accept the standard image tile requests from Deep Zoom and give it the images from Virtual Earth. I have done this before to show how to render other formats of imagery onto Virtual Earth.
This is the handler:
using System.Web; using System.Web.Services; using System.Text; using System.Drawing; using System.Drawing.Imaging; using System; using System.IO; using System.Net; namespace SoulSolutions.VETileProxy { [WebService(Namespace = "http://soulsolutions.com.au/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class VETileRequest : IHttpHandler { private string Protocol = "http://"; private string TilePath = ".ortho.tiles.virtualearth.net/tiles/"; private string Prefix = "h"; private string Suffix = ".jpeg?g=159"; public void ProcessRequest(HttpContext context) { string url = context.Request.Url.ToString().ToLower(); string[] items = url.Split('/'); if (items.Length > 2) { //Extract the Deep Zoom Level from the path, DZ 9 = VE 1 int DZLevel; if (int.TryParse(items[items.Length - 2], out DZLevel)) { int zoom = DZLevel - 8; if (zoom > 0) { //Extract the row and column number of the tile from the filename (7_4.jpg) //cut off 5 chr off the end assuming it will be .jpeg or //TODO: .png. string[] parts = items[items.Length - 1].Substring(0, items[items.Length - 1].Length - 4).Split('_'); int row; int col; if (int.TryParse(parts[0], out row) && int.TryParse(parts[1], out col)) { //Create the VE tile request path string QuadKey = TileXYToQuadKey(row, col, zoom); string VEUrl = Protocol + Prefix + QuadKey[QuadKey.Length - 1] + TilePath + Prefix + QuadKey + Suffix; //Redirect to that tile - if only this worked we would need to proxy :( //context.Response.Redirect(VEUrl); //We have to download the tiles and then reserve it to deep zoom Bitmap wmsBitmap = DownloadImage(VEUrl); //Spit out to the output window what we are doing System.Diagnostics.Debug.WriteLine(VEUrl); Bitmap tileBitmap = new Bitmap(256, 256); Graphics graphics = Graphics.FromImage(tileBitmap); graphics.DrawImage(wmsBitmap, 0, 0, 256, 256); graphics.Dispose(); // Stream the image out in response to the HTTP request MemoryStream streamImage = new MemoryStream(); tileBitmap.Save(streamImage, ImageFormat.Jpeg); context.Response.Clear(); context.Response.ContentType = "image/jpeg"; context.Response.Cache.SetCacheability(HttpCacheability.Public); context.Response.Cache.SetExpires(DateTime.Now.AddDays(7)); context.Response.AddHeader("content-length", System.Convert.ToString(streamImage.Length)); context.Response.BinaryWrite(streamImage.ToArray()); } } else //Need to serve up the versions of the earth smaller then that made by VE (<512x512). { //Pass through to real jpeg Image img = Image.FromFile(context.Server.MapPath(context.Request.FilePath)); //Prepare the response context.Response.ContentType = "image/jpeg"; context.Response.Cache.SetCacheability(HttpCacheability.Public); context.Response.Cache.SetExpires(DateTime.Now.AddDays(7)); img.Save(context.Response.OutputStream, ImageFormat.Jpeg); img.Dispose(); } } } context.Response.Write("Request not valid"); } public bool IsReusable { get { return true; } } private static string TileXYToQuadKey(int tileX, int tileY, int levelOfDetail) { StringBuilder quadKey = new StringBuilder(); for (int i = levelOfDetail; i > 0; i--) { char digit = '0'; int mask = 1 << (i - 1); if ((tileX & mask) != 0) { digit++; } if ((tileY & mask) != 0) { digit++; digit++; } quadKey.Append(digit); } return quadKey.ToString(); } private Bitmap DownloadImage(string url) { WebClient webClient = new WebClient(); byte[] baImage = webClient.DownloadData(url); MemoryStream memoryStream = new MemoryStream(baImage); Bitmap bitmap = (Bitmap)Bitmap.FromStream(memoryStream); memoryStream.Close(); return bitmap; } } }
A work in progress yes, but the results are very cool. I had hoped that I could simply do a redirect to the VE tile as you may see in the code. This didn’t work. So I’m forced to have my server download and reserve the tiles. This means I can’t give you a live example but find below the whole project that you can run locally and play with yourself.
You may have noticed in the video and the code that for requests below 512x512pixels or level 8 I serve up some static images. I got Deep Zoom Composer to make these for me as the VE tile server does not go that small.
So next up is to add the other layers and tidy up the handler to do these nicely. Then we can move onto fun things like adding Virtual Earth functionality to Silverlight and connecting to these upcoming SOAP web services from the VE platform.
An issue is the one pixel overlap, VE doesn’t overlap and I’ve found setting it to zero in the xml crashes my browser. More trial and error needed.
Are you interested in a Silverlight Virtual Earth codeplex project? A single open source project where we can bring together the best parts and create something cool while we wait for Microsoft to make something? Send me an email to John at soulsolutions.com.au if your interested or leave a comment if you just want more.