Attention: We are retiring the ASP.NET Community Blogs. Learn more >

Conquering Deep Zoom (Part 2), serving tiles, custom MultiScaleTileSource

When we last left our heroes, we talked a bit about cutting up a single image for the purpose of serving tiles to a MultiScaleImage (Deep Zoom) in Silverlight. The motivation in this case is to offer some other means of serving the images, like from a database, instead of the mess of files that Deep Zoom Composer generates.

I kind of glossed over the whole data aspect of this, as you can see from the last entry that the TileCreated handler was a little vague...

private void maker_TileCreated(object sender, TileCreatedEventArgs e)
{
    DataContext context = new DataContext();
    var tile = e.TileContainer;
    Tile tileData = new Tile { ImageID = _imageID, Level = tile.Level, TilePositionX = tile.TilePositionX, TilePositionY = tile.TilePositionY, ImageData = new System.Data.Linq.Binary(tile.ImageData) };
    context.Tiles.InsertOnSubmit(tileData);
    context.SubmitChanges();
}

(I also left out the TileContainer class, but you can infer what it looks like from the Tile Maker and how I assign its properties to the data Tile.) So to fill in the blanks a little, I wanted to share what the DataContext looks like in this case.

As you can see, this isn't very complicated. The Image table is something I'm still thinking about and messing with for integration into another project, so you probably only need to worry about the width, height and ID. The Tile table is a bit more useful. Note the multiple fields specified for the primary key. I'm not crazy about this arrangement, but LINQ won't allow you to insert without a primary key, and the only way to represent a unique combination is to specify this combination or add a totally useless new identity field. In an ideal world, I just assume have a clustered index on ImageID.

Given this data arrangement, hopefully the event handler listed above for the last entry's TileMaker class makes sense.

Now that we have images in the database, we need to pull them back out using an HttpHandler. Specifically, we can use an asynchronous HttpHandler so as not to tie up the thread pool with a bazillion image requests. If the database is indexed right and you've got a nice balance between tile size and number of tiles per image, I'm not sure this is totally necessary, but I suppose it's a good practice. Hopefully I'm doing it right!

public class TileHandler : IHttpAsyncHandler
{
    private AsyncTaskDelegate _asyncDelegate;
    protected delegate void AsyncTaskDelegate(HttpContext context);

    public void ProcessRequest(HttpContext context)
    {
        int id;
        int level;
        int x;
        int y;
        var queryString = context.Request.QueryString;
        if (int.TryParse(queryString["id"], out id)
            && int.TryParse(queryString["level"], out level)
            && int.TryParse(queryString["x"], out x)
            && int.TryParse(queryString["y"], out y))
        {
            var data = new Data.DataContext();
            var image = data.Tiles.Single(t => t.ImageID == id && t.Level == level && t.TilePositionX == x && t.TilePositionY == y);
            if (image == null)
                context.Response.StatusCode = 404;
            else
            {
                context.Response.ContentType = "image/jpeg";
                var bytes = image.ImageData.ToArray();
                context.Response.OutputStream.Write(bytes, 0, bytes.Length);
            }
        }
        else
            context.Response.StatusCode = 404;
    }

    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
    {
        _asyncDelegate = ProcessRequest;
        return _asyncDelegate.BeginInvoke(context, cb, extraData);
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        _asyncDelegate.EndInvoke(result);
    }

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

The meat of the code is of course the ProcessRequest method. It checks to see that all of the required query string parameters are there, and if so, continues to try and load the image using the same LINQ context we showed above. If the image is found, it is streamed out to the output stream of the handler. Again, hopefully I'm getting the async part of this rigth.

The MultiscaleImage by default wants to find a Deep Zoom XML file and a bunch of actual files, so we need to tell it how we want to load the tiles. To do this, we'll add a class to our Silverlight project called ViewerSource, and we'll define it like this:

public class ViewerSource : MultiScaleTileSource
{
    /// <summary>
    /// Constructs a new tile source for a multi scale image by indicating its overall size and a string used to generate
    /// URL's that will return the tile images.
    /// </summary>
    /// <param name="imageWidth">Width of the source image.</param>
    /// <param name="imageHeight">Height of the source image.</param>
    /// <param name="tileSize">Tile size (square on one side) in pixels.</param>
    /// <param name="imageID">ID of the image to pass into the URL.</param>
    /// <param name="urlToFormat">URL to be formatted with ID, tile level, tilePositionX and
    /// tilePositionY, using {0}, {1}, {2} and {3}, respectively.
    /// </param>
    public ViewerSource(int imageWidth, int imageHeight, int tileSize, int imageID, string urlToFormat)
        : base(imageWidth, imageHeight, tileSize, tileSize, 0)
    {
        ImageID = imageID;
        UrlToFormat = urlToFormat;
    }

    public int ImageID { get; private set; }
    public string UrlToFormat { get; private set; }

    protected override void GetTileLayers(int tileLevel, int tilePositionX, int tilePositionY, System.Collections.Generic.IList<object> tileImageLayerSources)
    {
        string source = String.Format(UrlToFormat, ImageID, tileLevel, tilePositionX, tilePositionY);
        Uri uri = new Uri(source, UriKind.Absolute);
        tileImageLayerSources.Add(uri);
    }
}

MultiScaleTileSource is an abstract class with one method you have to define, GetTileLayers. MultiScaleImage calls this method to obtain the location of the tiles, with numbers you may find familiar from the mess of Deep Zoom Composer generated files: the tileLevel (or zoom level), and the tile's X and Y positions in the image. We're formatting the URL based on whatever was passed in from the constructor, in this case a URL that our HttpHandler is going to recognize. So the calling code in your XAML code-behind will look something like this, where Viewer is the name of the MultiScaleImage control:

Viewer.Source = new ViewerSource(width, height, tileSize, id, urlToFormat);

So you're probably wondering where the URL comes from, and that'll be in the third part, where I wire up this stuff to Javascript in the browser. For now, know that the URL I'm passing to the ViewerSource constructor is:

http://localhost:2974/TileHandler.ashx?id={0}&level={1}&x={2}&y={3}

Even without the wiring in the actual Silverlight control, you can probably see where this is going. In the last part of this series, I'll go over the hell of figuring out how to center the image and reset its zoom level.

3 Comments

Comments have been disabled for this content.