July 2009

Volume 24 Number 07

Wicked Code - Taking Silverlight Deep Zoom To The Next Level

By Jeff Prosise | July 2009

Contents

Fixing Composer's Panning Logic
Accessing Sub-Images and Metadata
Dynamic Deep Zoom: Supplying Image Pixels at Run Time
DeepZoomTools.dll

After Silverlight Deep Zoom was introduced to the world at MIX 2008, the buzz surrounding it persisted for weeks. An outgrowth of the Seadragon project at Microsoft Live Labs, Deep Zoom is a Silverlight adaptation of a technology for presenting vast amounts of pictorial data to users in a highly bandwidth-efficient manner. Sister adaptations that target Windows Mobile and AJAX are available and serve to increase the reach of the platform.

If you haven't seen Deep Zoom before, drop what you're doing and visit the canonical Deep Zoom site at memorabilia.hardrock.com. Use the mouse to pan around in the scene and the mouse wheel to zoom in and out. Thanks to Deep Zoom, you don't have to download gigabytes (or terabytes) of imagery to browse the Hard Rock Café's vast memorabilia collection. Deep Zoom downloads only the pixels it needs at the resolution it needs, and in Silverlight, the complexity of Deep Zoom is masked behind a remarkable control named MultiScaleImage. Once a Deep Zoom scene is composed (typically with Deep Zoom Composer, which you can download for free from go.microsoft.com/fwlink/?LinkId=148861), presenting the scene in a browser requires little more than declaring a MultiScaleImage control and pointing the control's Source property to Deep Zoom Composer's output. Supporting interactive panning and zooming requires a little mouse-handling code that interacts with the control, but these days Deep Zoom Composer will even provide that for you.

Despite the ease with which a basic Deep Zoom application can be built, you're missing out on the true richness of Deep Zoom if you go no further than Deep Zoom Composer takes you. Did you know, for example, that you can programmatically manipulate the images in a Deep Zoom scene, that you can create metadata and associate it with each image, or that Deep Zoom images can come from a database or be composed on the fly? Some of the truly remarkable Deep Zoom applications out there rely on a little-known feature of Deep Zoom that adds a whole new dimension to the platform.

If you care to take Silverlight Deep Zoom to the next level, here are three ways to do just that.

Fixing Composer's Panning Logic

First things first: if you want to get more out of Deep Zoom, the first thing you should know is not to trust the mouse-handling code emitted by Deep Zoom Composer. The code that pans around the scene in response to MouseMove events "loses" the mouse if you pan too quickly. Try it. Take any Deep Zoom app created by Deep Zoom Composer and position the mouse cursor over an identifiable point or pixel in the scene. Then move the mouse quickly back and forth and up and down a few times. Observe that when you stop and the scene springs back to the cursor position, the cursor is no longer located at the point it was when you started. The more and faster you move, the greater the difference. It's not a deal breaker, but try the same experiment on the Hard Rock Memorabilia site and you'll find that the scene reliably snaps back to the original cursor position no matter how hard you try to fool it.

Figure 1 shows how to modify Deep Zoom Composer's code to fix the problem. First, declare two new fields named lastViewportOrigin and lastMousePosition in the Page class in Page.xaml.cs. (While you're at it, delete the fields named dragOffset and currentPosition, because they're not needed.) Then rewrite the MouseLeftButtonDown and MouseMove handlers as shown. You'll find that the scene snaps back to precisely the original cursor position when you stop moving the mouse, and if you're as fastidious as I am about these things, you'll be able to sleep at night once more.

Figure 1 Fixing Deep Zoom Composer's Panning Code

Point lastViewportOrigin; Point lastMousePosition;...this.MouseLeftButtonDown += delegate(object sender, MouseButtonEventArgs e) { mouseButtonPressed = true; mouseIsDragging = false; lastViewportOrigin = msi.ViewportOrigin; lastMousePosition = e.GetPosition(msi); }; this.MouseMove += delegate(object sender, MouseEventArgs e) { if (mouseIsDragging) { Point pos = e.GetPosition(msi); Point origin = lastViewportOrigin; origin.X += (lastMousePosition.X - pos.X) / msi.ActualWidth * msi.ViewportWidth; origin.Y += (lastMousePosition.Y - pos.Y) / msi.ActualWidth * msi.ViewportWidth; msi.ViewportOrigin = lastViewportOrigin = origin; lastMousePosition = pos; } };

Accessing Sub-Images and Metadata

You may have noticed that when you export a project from Deep Zoom Composer, you're offered the choice of exporting as a composition or as a collection The latter option comes with one very desirable benefit: rather than exporting a Deep Zoom scene containing all the images you added lumped together into one monolithic image, it exports a scene containing individually addressable sub-images. The sub-images are exposed through the MultiScaleImage control's SubImages property, and because they are individually addressable objects, the sub-images can be manipulated, animated, and fumigated (just kidding!) to add sparkle and interactivity to Deep Zoom applications.

Each item in the SubImages collection is an instance of MultiScaleSubImage, which derives from DependencyObject and includes the properties AspectRatio, Opacity, ZIndex, ViewportOrigin, and ViewportWidth. The latter two combine to determine a sub-image's size and position in a Deep Zoom scene. Be aware that when a MultiScaleImage control first loads, its SubImages property is empty. Your first opportunity to iterate over the sub-images is when the control fires its ImageOpenSucceeded event.

One use for the SubImages property is to hit-test individual images in order to display metadata—titles, descriptions, and so forth—in response to clicks or mouseovers. Another use for it is to programmatically rearrange the images in a Deep Zoom scene. The DeepZoomTravelDemo application shown in Figure 2demonstrates how to do both. When you position the mouse over one of the images in the scene, a partially transparent information panel appears on the right containing an image title and description. And when you click the Shuffle button in the upper left corner, the images rearrange themselves in random order.

Figure 2 DeepZoomTravelDemo

The nine images featured in DeepZoomTravelDemo are photos I snapped on some of my overseas trips. I imported them into Deep Zoom Composer, arranged them in a grid, and exported the scene (making sure to select "Export as Collection"). Then I imported the output from Deep Zoom Composer into a Silverlight project and added zooming and panning logic similar to that in the preceding section. To keep the download size manageable (13MB versus 170MB), I deleted the bottom two layers of the image pyramid that Composer generated before I uploaded the app to the MSDN Code Gallery. The version that you download works just fine, but when you zoom, the images get grainy a lot quicker than they do in the original version.

Displaying image metadata as DeepZoomTravelDemo does presents two challenges to the developer. First, where do you store the metadata, and how do you associate it with images in the scene? Second, how do you correlate the items in the MultiScaleImage control's SubImages collection with images in the scene since the MultiScaleSubImage class provides no information relating the two?

The first task—storing the metadata—is accomplished by entering a text string into the Tag box displayed in the lower right corner of Deep Zoom Composer when an image is selected. I used it to store each image's title and description, separated by plus signs. Composer writes the tags to the Metadata.xml file created when you export the project. Each image in the scene is represented by an <Image> element in Metadata.xml, and each <Image> element contains a sub-element named <Tag> that contains the corresponding tag. Figure 3shows the <Image> element written into Metadata.xml for the image in the upper left corner of the scene. Composer's tag editing interface is somewhat clumsy since the Tag box is so small, but you can always edit the Metadata.xml file manually as I did to tag each image with a title and description.

Figure 3 An &lt;Image&gt; Element in Metadata.xml

<Image> <FileName> C:\Users\Jeff\Documents\Expression\Deep Zoom ComposerProjects\DeepZoomTravelDemo\source images\great wall of china.jpg </FileName> <x>0</x> <y>0</y> <Width>0.316957210776545</Width> <Height>0.313807531380753</Height> <ZOrder>1</ZOrder> <Tag> Great Wall of China+The Great Wall of China near Badaling, about an hour north of Beijing. This portion of the Great Wall has been restored and offers outstanding views of the surrounding mountains. </Tag> </Image>

It would be great if the MultiScaleSubImage class had a Tag property that was automatically initialized with the content of the <Tag> element; but it doesn't, so you have to improvise. First, you can write a bit of code that downloads Metadata.xml and parses the tags from it. Second, you can use the <ZOrder> elements in Metadata.xml to correlate <Image> elements with images in the Deep Zoom scene. If the scene contains nine images (and the MultiScaleImage control's SubImages collection therefore contains nine MultiScaleSubImage objects), SubImages[0] corresponds to the image whose <ZOrder> is 1, SubImages[1] corresponds to the image whose <ZOrder> is 2, and so on.

DeepZoomTravelDemo uses this correlation to store image titles and descriptions. At startup, the Page constructor uses a WebClient object to initiate an asynchronous download of Metadata.xml from the server's ClientBin folder (see Figure 4). When the download is complete, the WebClient_OpenReadCompleted method parses the downloaded XML with an XmlReader and initializes the field named _Metadata with an array of SubImageInfo objects containing information about the images in the scene, including titles and descriptions. The class is shown here:

public class SubImageInfo { public string Caption { get; set; } public string Description { get; set; } public int Index { get; set; } }

Figure 4 Downloading Metadata.xml and Correlating Metadata with Sub-Images

private SubImageInfo[] _Metadata;...public Page() { InitializeComponent(); // Registermousewheel event handler HtmlPage.Window.AttachEvent("DOMMouseScroll ", OnMouseWheelTurned); HtmlPage.Window.AttachEvent("onmousewheel ", OnMouseWheelTurned); HtmlPage.Document.AttachEvent("onmousewheel ", OnMouseWheelTurned); // Fetch Metadata.xml from the server WebClient wc = new WebClient(); wc.OpenReadCompleted += new OpenReadCompletedEventHandler(WebClient_OpenReadCompleted); wc.OpenReadAsync(new Uri("Metadata.xml ", UriKind.Relative)); } private void WebClient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e) { if (e.Error != null) { MessageBox.Show("Unable to load XML metadata "); return; } // Create a collection of SubImageInfo objects from Metadata.xml List < SubImageInfo > images = new List < SubImageInfo > (); try { XmlReader reader = XmlReader.Create(e.Result); SubImageInfo info = null; while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element && reader.Name == "Image ") info = new SubImageInfo(); else if (reader.NodeType == XmlNodeType.Element && reader.Name == "ZOrder ") info.Index = reader.ReadElementContentAsInt(); else if (reader.NodeType == XmlNodeType.Element && reader.Name == "Tag ") { string[] substrings = reader.ReadElementContentAsString().Split('+'); info.Caption = substrings[0]; if (substrings.Length > 1) info.Description = substrings[1]; else info.Description = String.Empty; } else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "Image ") images.Add(info); } } catch (XmlException) { MessageBox.Show("Error parsing XML metadata "); } // Populate the _Metadata array with ordered data _Metadata = new SubImageInfo[images.Count]; foreach(SubImageInfo image in images) _Metadata[image.Index - 1] = image; }

The <ZOrder> values read from Metadata.xml are used to order the SubImageInfo objects in the _Metadata array, ensuring that the order of the items in the _Metadata array is identical to the order of the items in MultiScaleImage's SubImages collection. In other words, _Metadata[0] contains the title and description for SubImages[0], _Metadata[1] contains the title and description for SubImages[1], and so on. Incidentally, I used XmlReader rather than LINQ to XML to avoid increasing the size of the XAP file by introducing an extra assembly required by LINQ to XML (System.Xml.Linq.dll).

Now that _Metadata is initialized with SubImageInfo objects containing titles and descriptions, the next step is to write the code to display the titles and descriptions. That happens in Figure 5. The MouseMove handler that pans the Deep Zoom scene if the left mouse button is down behaves differently if the left mouse button is up: it hit-tests the scene to determine whether the cursor is currently over one of the sub-images. Hit testing is performed by the helper method named GetSubImageIndex, which returns -1 if the cursor isn't over a sub-image or a 0-based image index if it is. That index identifies both a sub-image in MutliScaleImage.SubImages and a SubImageInfo object in _Metadata. A few lines of code copies the title and description from the SubImageInfo object to a pair of TextBlocks and one more line of code triggers an animation that displays the information panel if it's not already displayed. Note that GetSubImageIndex checks the sub-images for hits in reverse order since the final sub-image in the MultiScaleimage control's SubImages collection is highest in the Z-order, the next-to-last sub-image is second highest in the Z-order, and so on.

Figure 5 Hit-Testing the Sub-Images

private int _LastIndex = -1;...private void MSI_MouseMove(object sender, MouseEventArgs e) { if (_Dragging) { // If the left mouse button is down, pan the Deep Zoom scene ... } else { // If the left mouse button isn't down, update the infobar if (_Metadata != null) { int index = GetSubImageIndex(e.GetPosition(MSI)); if (index != _LastIndex) { _LastIndex = index; if (index != -1) { Caption.Text = _Metadata[index].Caption; Description.Text = _Metadata[index].Description; FadeIn.Begin(); } else { FadeOut.Begin(); } } } } } private int GetSubImageIndex(Point point) { // Hit-test each sub-image in the MultiScaleImage control to determine // whether "point " lies within a sub-image for (int i = MSI.SubImages.Count - 1; i > = 0; i--) { MultiScaleSubImage image = MSI.SubImages[i]; double width = MSI.ActualWidth / (MSI.ViewportWidth * image.ViewportWidth); double height = MSI.ActualWidth / (MSI.ViewportWidth * image.ViewportWidth * image.AspectRatio); Point pos = MSI.LogicalToElementPoint(new Point(-image.ViewportOrigin.X / image.ViewportWidth, -image.ViewportOrigin.Y / image.ViewportWidth)); Rect rect = new Rect(pos.X, pos.Y, width, height); if (rect.Contains(point)) { // Return the image index return i; } } // No corresponding sub-image return -1; }

In addition to supporting mouseovers, DeepZoomTravelDemo lets you rearrange the images in the scene. If you haven't already, try clicking the Shuffle button in the upper left corner of the scene. (In fact, click it several times; the images will assume a different order each time.) The rearranging is performed by the Shuffle method in Figure 6, which creates an array containing all the images' ViewportOrigins, reorders the array using a random-number generator, and then creates a Storyboard and a series of PointAnimations to move the sub-images to the positions contained in the reordered array. The key here is that the MultiScaleimage control's SubImages property exposes the sub-images to your code, and you can modify a sub-image's ViewportOrigin property to change its position in the scene.

Figure 6 Shuffling the Sub-Images

private void Shuffle() { // Create a randomly ordered list of sub-image viewport origins List < Point > origins = new List < Point > (); foreach(MultiScaleSubImage image in MSI.SubImages) origins.Add(image.ViewportOrigin); Random rand = new Random(); int count = origins.Count; for (int i = 0; i < count; i++) { Point origin = origins[i]; origins.RemoveAt(i); origins.Insert(rand.Next(count), origin); } // Create a Storyboard and animations for shuffling Storyboard sb = new Storyboard(); for (int i = 0; i < count; i++) { PointAnimation animation = new PointAnimation(); animation.Duration = TimeSpan.FromMilliseconds(250); animation.To = origins[i]; Storyboard.SetTarget(animation, MSI.SubImages[i]); Storyboard.SetTargetProperty(animation, new PropertyPath("ViewportOrigin ")); sb.Children.Add(animation); } // Run the animations sb.Begin(); }

As I researched this column, I found several blog entries that provided helpful information. One was Jaime Rodriquez's " Working with Collections in Deep Zoom." Another was " Deep Zoom Composer—Filtering by Tag Sample", which was written by a member of the Expression Blend team and presents a technique for filtering Deep Zoom images based on image tags. Implementing mouseovers, rearranging the images in a scene, and filtering images based on tag data are but a few of the features made possible by the ability to address the individual sub-images in a Deep Zoom scene and associate metadata with them.

Making Dynamic Deep Zoom Even Better

I have been fascinatedby fractals ever since I discovered them some 20 years ago. I wrote my first Mandelbrot viewer in the early 1990s, if memory serves me correctly. My bookshelf of oldie-but-goodie computer books still contains a pristine copy of a book titled Fractal Image Compression by Barnsley and Hurd that I used in a research project on data compression in the mid-90s. And one of my favorite books of all time is Chaos by James Gleick (Penguin, 2008).

Building an interactive and visually compelling Mandelbrot viewer for browsers is something I've wanted to do since the first day I laid eyes on Silverlight. Dynamic Deep Zoom made it possible. The downside, of course, is that the images are generated on the server and downloaded to the client, leading to unwanted latency and also increasing the load on the server.

Silverlight 2 doesn't include an API for generating bitmaps on the client, but you can generate them anyway using Joe Stegman's Silverlight PNG encoder. Minh Nguyen used it to build Mandelbrot Explorer, which you can read all about it in his blog article " Minh T. Nguyen's Mandelbrot Explorer 1.0 in Silverlight 2.0 with source code." Silverlight 3, which will be in beta by the time you read this, has a bitmap API, but the problem remains that Deep Zoom wants to pull images from the server. It is unclear at the moment whether the next version of Deep Zoom will have a client-side story, but if it does, you can bet that I'll be revising MandelbrotDemo for Silverlight 3 to work entirely on the client.

Dynamic Deep Zoom: Supplying Image Pixels at Run Time

Deep Zoom Composer's export feature generates all the data needed by a MultiScaleImage control. That data includes an XML file (dzc_output.xml) that references other XML files, which in turn reference the individual images in the scene. Composer's output also includes hundreds (sometimes thousands) of image tiles generated from those images. The tiles form an image pyramid, with each level of the pyramid containing a tiled version of the original image, and each level representing a different resolution. The level at the top of the pyramid, for example, might contain a single tile with a 256x256 rendition of the image. The next level down would contain four 256x256 tiles which, put together, form a 512x512 version of the image. The next level down would contain sixteen 256x256 tiles representing different parts of a 1,024x1,024 image, and so on. Deep Zoom Composer generates as many levels as necessary to depict the original image in its native resolution. As a user zooms and pans in a Deep Zoom Scene, the MultiScaleImage control is constantly firing off HTTP requests to the server to fetch image tiles at the proper resolution. It also does some slick blending work to smooth the transition from one level to another.

What you probably don't realize about the MultiScaleImage control is that it doesn't require Deep Zoom Composer. Composer is really just a tool for quickly and easily creating Deep Zoom projects that incorporate scenes built from static images. As an alternative to providing MultiScaleImage with static content, you can generate content at run time in response to requests from MultiScaleImage and download that content to the client.

Why would you ever need to generate Deep Zoom content at run time? Developers ask me how to do this all the time. "Is it possible to supply image data to Deep Zoom dynamically?" The reason is that it enables a whole new genre of Deep Zoom applications that fetch image tiles from databases and that generate image tiles on the fly.

Want an example? Check out the Deep Earth project and an example of Deep Earth at work at deepearth.soulsolutions.com.au/. Deep Earth is referred to as a mapping control powered by the combination of Microsoft's Silverlight 2 platform and the DeepZoom (MultiScaleImage) control. In other words, it's a control you can drop into a Silverlight application to expose the vast amount of geographic data available from Microsoft Virtual Earth through a Deep Zoom front end. You can start in outer space and zoom all the way down to your front lawn. And the zooming is amazingly smooth, thanks to the work done behind the scenes by MultiScaleImage and the Deep Zoom runtime.

Deep Earth isn't driven by a bunch of XML files and image tiles output by Deep Zoom Composer; it supplies image tiles to the MultiScaleImage control dynamically, and it fetches image tiles from Virtual Earth. Users of Deep Earth refer to this as "dynamic Deep Zoom."

The application depicted in Figure 7demonstrates the basics of dynamic Deep Zoom. MandelbrotDemo provides a Deep Zoom window into the Mandelbrot set—probably the most famous fractal in the world. The Mandelbrot set is infinitely complex, which means that you can zoom in forever and the level of detail will never decrease. Mandelbrot viewers are common in the software world, but few are as slick as the one that uses Deep Zoom. Try it; run MandelbrotDemo and zoom in on some of the swirling regions at the edge of the Mandelbrot set (at the boundary between black and bright colors). You can't zoom in forever because even a dynamic Deep Zoom scene has a finite width and height, but the scene's dimensions can be very, very large (up to 232 pixels per side).

fig09a.gif

Two Views of the Mandlebrot Set

fig09b.gif

The first step in implementing dynamic Deep Zoom is to derive from Silverlight's MultiScaleTileSource class, which is found in the System.Windows.Media namespace of System.Windows.dll, and override the GetTileLayers method. Each time the MultiScaleImage control needs a tile, it calls GetTileLayers. Your job is to create an image tile and return it to the MultiScaleImage control by adding it to the IList passed in GetTileLayers' parameter list. Other parameters input to GetTileLayers specify the zoom level (literally, the level of the image pyramid from which tiles are being requested) and the X and Y position within that level of the pyramid of the tile that is being requested. Just as X, Y, and Z values are sufficient to identify a point in 3D coordinate space, an X value, a Y value, and a level, uniquely identify an image tile in a Deep Zoom image pyramid.

Figure 8 shows the MultiScaleTileSource-derived class featured in MandelbrotDemo. The GetTileLayers override does little more than submit an HTTP request for the image tile to the server. The endpoint for the request is an HTTP handler named MandelbrotImageGenerator.ashx. Before we examine the handler, however, let's see how MandelbrotTileSource is wired up to a MultiScaleImage control.

Figure 8 MultiScaleTileSource Derivative

public class MandelbrotTileSource: MultiScaleTileSource { private int _width; // Tile width private int _height; // Tile height public MandelbrotTileSource(int imageWidth, int imageHeight, int tileWidth, int tileHeight): base(imageWidth, imageHeight, tileWidth, tileHeight, 0) { _width = tileWidth; _height = tileHeight; } protected override void GetTileLayers(int level, int posx, int posy, IList < object > sources) { string source = string.Format("http://localhost:50216/MandelbrotImageGenerator.ashx? " + "level={0}&x={1}&y={2}&width={3}&height={4} ", level, posx, posy, _width, _height); sources.Add(new Uri(source, UriKind.Absolute)); } }

Figure 9 shows an excerpt from MandelbrotDemo's Page.xaml.cs file—specifically, the XAML code-behind class's constructor. The key statement is the one that creates a MandelbrotTileSource object and assigns a reference to it to the MultiScaleImage control's Source property. For static Deep Zoom, you set Source to the URI of dzc_output.xml. For dynamic Deep Zoom, you point it to a MultiScaleTileSource object instead. The MandelbrotTileSource object created here specifies that the image being served up measures 230 pixels on each side and is divided into 128x128-pixel tiles.

Figure 9 Registering a Deep Zoom Tile Source

public Page() { InitializeComponent(); // Point MultiScaleImage control to dynamic tile source MSI.Source = new MandelbrotTileSource((int) Math.Pow(2, 30), (int) Math.Pow(2, 30), 128, 128); // Register mousewheel event handler HtmlPage.Window.AttachEvent("DOMMouseScroll ", OnMouseWheelTurned); HtmlPage.Window.AttachEvent("onmousewheel ", OnMouseWheelTurned); HtmlPage.Document.AttachEvent("onmousewheel ", OnMouseWheelTurned); }

The work of generating the image tiles is performed by MandelbrotImageGenerator.ashx back on the server (see Figure 10). After retrieving input parameters from the query string, it creates a bitmap depicting the requested tile and writes the image bits into the HTTP response. DrawMandelbrotTile does the pixel generation. When called, it converts the X-Y-level value identifying the image tile that was requested into coordinates in the complex plane (a mathematical plane in which real numbers are graphed along the X axis and imaginary numbers—numbers that incorporate the square root of -1—are graphed along the Y axis). Then it iterates through all the points in the complex plane that correspond to pixels in the image tile, checking each point to determine whether it belongs to the Mandelbrot set and assigning the corresponding pixel a color representing its relationship to the Mandelbrot set (more on this in a moment).

Figure 10 HTTP Handler for Generating Deep Zoom Image Tiles

public class MandelbrotImageGenerator: IHttpHandler { private const int _max = 128; //Maximum number of iterations private const double _escape = 4; // Escape value squared public void ProcessRequest(HttpContext context) { // Grab input parameters int level = Int32.Parse(context.Request["level "]); int x = Int32.Parse(context.Request["x "]); int y = Int32.Parse(context.Request["y "]); int width = Int32.Parse(context.Request["width "]); int height = Int32.Parse(context.Request["height "]); // Generate the bitmap Bitmap bitmap = DrawMandelbrotTile(level, x, y, width, height); // Set the response's content type to image/jpeg context.Response.ContentType = "image/jpeg "; // Write the image to the HTTP response bitmap.Save(context.Response.OutputStream, ImageFormat.Jpeg); // Clean up and return bitmap.Dispose(); } public bool IsReusable { get { return true; } } private Bitmap DrawMandelbrotTile(int level, int posx, int posy, int width, int height) { // Create a bitmap to represent the requested tile Bitmap tile = new Bitmap(width, height); // Compute the number of tiles in each direction at this level int cx = Math.Max(1, (int) Math.Pow(2, level) / width); int cy = Math.Max(1, (int) Math.Pow(2, level) / height); // Compute starting values for real and imaginary components // (from -2.0 - 1.5i to 1.0 + 1.5i) double r0 = -2.0 + (3.0 * posx / cx); double i0 = -1.5 + (3.0 * posy / cy); // Compute increments for real and imaginary components double dr = (3.0 / cx) / (width - 1); double di = (3.0 / cy) / (height - 1); // Iterate by row and column checking each pixel for // inclusion in the Mandelbrot set for (int x = 0; x < width; x++) { double cr = r0 + (x * dr); for (int y = 0; y < height; y++) { double ci = i0 + (y * di); double zr = cr; double zi = ci; int count = 0; while (count < _max) { double zr2 = zr * zr; double zi2 = zi * zi; if (zr2 + zi2 > _escape) { tile.SetPixel(x, y, ColorMapper.GetColor(count, _max)); break; } zi = ci + (2.0 * zr * zi); zr = cr + zr2 - zi2; count++; } if (count == _max) tile.SetPixel(x, y, Color.Black); } } // Return the bitmap return tile; } }

Sadly, there is virtually no documentation on Silverlight's MultiScaleTileSource class. Lest you think I'm a genius for figuring all this out (anyone that knows me will attest that I am not), let me give credit where credit is due. As I wrestled with the meaning of the input parameters and how to map Deep Zoom X-Y-level values to the complex plane, I found an excellent blog post by Mike Ormond, Deep Zoom, MultiScaleTileSource and the Mandelbrot Set. His post provided key insights into dynamic Deep Zoom and also referenced another blog post, The Mandelbrot Set, that describes an efficient approach to computing the Mandelbrot set. My work was probably halved by work others had done before me.

One final note on my implementation: virtually every application that renders the Mandelbrot set uses a different color scheme. I chose a scheme that assigns pixels representing coordinates that belong to the Mandelbrot set black, and pixels representing coordinates outside the Mandelbrot set RGB colors. The further a coordinate lies from the Mandelbrot set, the "cooler" or bluer the color; the closer it lies to the Mandelbrot set, the "hotter" the color. Distance from the Mandelbrot set is determined by how rapidly the point escapes to infinity. In the code here, this is the number of iterations it takes DrawMandelbrotTile's while loop to determine that the point is not part of the Mandelbrot set. The fewer the iterations, the further the point lies from the set of points that make up the Mandelbrot set. I factored the code that generates an RGB color value from the iteration count into a separate class named ColorMapper ( Figure 11). If you want to experiment with different color schemes, simply modify the GetColor method. You can see the results of the gray-scale rendering by doing the following:

int val = (count * 255) / max; return Color.FromArgb(val, val, val);

Figure 11 ColorMapper Class

public class ColorMapper { public static Color GetColor(int count, int max) { int h = max >> 1; // Divide max by 2 int q = max >> 2; // Divide max by 4 int r = (count * 255) / max; int g = ((count % h) * 255) / h; int b = ((count % q) * 255) / q; return Color.FromArgb(r, g, b); } }

DeepZoomTools.dll

A final tidbit of information regarding Deep Zoom that you might find useful involves an assembly named DeepZoomTools.dll. Deep Zoom Composer uses this assembly to generate tiled images and metadata from the scenes that you build. In theory, you could use it to build composition tools of your own. I say "in theory" because there's precious little out there in terms of documentation. Find out more about DeepZoomTools.dll on the Expression Blend and Designblog. And shoot me an e-mail if you come up with some unique, creative uses for Deep Zoom but aren't quite sure how to make it do what you want it to do.

Send your questions and comments for Jeff to wicked@microsoft.com.

Jeff Prosise is a contributing editor to MSDN Magazineand the author of several books, including Programming Microsoft .NET(Microsoft Press, 2002). He's also cofounder of Wintellect ( www.wintellect.com), a software consulting and education firm that specializes in Microsoft .NET. Have a comment on this column? Contact Jeff at wicked@microsoft.com.