Saturday, April 05, 2008

Silverlight, Deep Zoom, Collections and Hit Testing

Since Silverlight 2 Beta 1 was released at Mix, Deep Zoom has attracted a lot of interesting, due in large part to the excellent Hard Rock Café Memorabilia site. The Deep Zoom Composer tool is an excellent start, and makes it fairly trivial to create single large deep zoom images, and there are numerous samples around that show how to implement the panning and zooming features, but thus far, there's been almost no examples of how to manipulate collections, which is the most interesting part of the Hard Rock demo.

DZC can easily create collections, and using them is initially as easy as pointing at the generated items.bin file (instead of the info.bin file for a single image) but actually manipulating this collection requires a bit more code (although not much).

I found the trickiest thing was understanding how the MSI handled its coordinate space. Rather than using an absolute coordinate space it seems to always use a relative space, which makes it a little tricker to manipulate multiple images.

The first thing to understand is that images in a collection don't seem to have the concept of an absolute pixel size. Like the single MSI, subimages are sized according their ViewportWidth, but the wrinkle is that their ViewportOrigin, which sets the image's position, is relative to its ViewportWidth as well - not the parent MSI element's ViewportWidth. So a ViewportWidth of 1 will fill (by width) an image whose main ViewportWidth is 1 and a viewportWidth of 2 will be half the width. Also, a ViewportOrigin of (1,1) will map to a different position within the main element if the ViewportWidth is different. Which is why I got confused the first time I tried to line up all the subimages, and found that some overlapped the others.

One thing I wanted to be able to do is work out which subimage the mouse is over (to pop up a tooltip or update other parts of the UI with contextual information). There isn't a useful subimage equivalent to ElementPointToLogicalPoint which works with subimages, and they don't expose a HitTest method, so you have to roll your own code for things like hit testing. Here's the code I used:


/// Given an element point relative to the DeepZoom image
/// return whether the point hits any subimages in the collection
/// parentImage: Parent image whose subimages we want
/// to test
/// p: Point to test (in element coordinate space)
/// imageindex: index into the SubImages collection
/// returns: true if a hit was found
bool HitTest(MultiScaleImage parentImage, Point p, ref int imageindex)
{
bool gotHit = false;
for (int i = 0; i < parentImage.SubImages.Count; i++)
{
MultiScaleSubImage image = parentImage.SubImages[i];

// Start with the logical origin of the image
Point topLeft = image.ViewportOrigin;

// Relative to the parent image, this coordinate is
// scaled by ViewportWidth (and the coords are negative)
topLeft.X = -(topLeft.X / image.ViewportWidth);
topLeft.Y = -(topLeft.Y / image.ViewportWidth);

// Calculate the logical width relative to the parent
double width = 1 / image.ViewportWidth;
// And get the height from the aspect ratio
double height = width / image.AspectRatio;

// Create a point representing the bottom left logical point
Point bottomright = new Point(topLeft.X + width, topLeft.Y + height);

// Now we've got the topleft and bottom right points
// in coordinates relative to the parent MultiScaleImage
// We can now use LogicalToElement to convert to Silverlight
// coordinates
topLeft = parentImage.LogicalToElementPoint(topLeft);
bottomright = parentImage.LogicalToElementPoint(bottomright);

// Now do the hit test
Rect r = new Rect(topLeft, bottomright);
if (r.Contains(p))
{
gotHit = true;
imageindex = i;
}
}
return gotHit;
}


Note that this code calculates the screen coords of the bounding rectangle for subimages, so you can also use this maths to place other visual elements like labels or frames at the right place.

One thing to remember, though, is that if you've set the UseSprings property to True, it's possible to hit the images as they are moving. You might want to restrict your hit testing by hooking the MotionFinished event and setting your visual elements in that handler. Otherwise there's a danger that you'll set them while the image is in motion, and when it stops they'll be in the wrong place.


Next time: Shuffling the collection.

No comments: