Jaime Rodriguez On Windows Phone, Windows Presentation Foundation, Silverlight and Windows 7
In the Deep Zoom run-time, you can load two types of compositions:
Collections have a lot more flexibility of course, but I also caution of two tiny concerns:
This post is about working with Collections, so let's assume I decided the two issues above are not in play (that is the case for most people). To find out what we can do with a MultiScaleSubImage, all we need is to look at the class definition:
A few of the less obvious options for dealing with collections are:
Tags If you have used Deep Zoom Composer you might have noticed that composer has a property called "Tag" for each image. As of beta2 tag is not exposed via the MultiScaleSubImage. So, how can you get to this Tag?
The tags are stored in the metadata.xml file generated by Deep Zoom Composer. You can easily get to this file using WebClient or HttpWebRequest networking APIs in SL2. It is a trivial two step process:
void wc_DownloadStringCompleted(objectsender, DownloadStringCompletedEventArgs e) { if(e.Cancelled == false&& e.Error == null) { strings = e.Result; XDocument doc = XDocument.Parse(s); var images = froma indoc.Element("Metadata").Descendants("Image") selecta; foreach ( XElement image inimages ) { CollectionImage ci = newCollectionImage { Height = (double) image.Element("Height"), Width = (double) image.Element("Width"), //here we read the ZOrder from metadata.xml and subtract one ZOrder = ((int) image.Element("ZOrder")) -1 , Tag = (string) image.Element("Tag"), Location = newPoint{ X = (double)image.Element("x"), Y = (double) image.Element("y")} } ; //here we map from the SubImages collection based on the ZOrder we read ci.Image = msi.SubImages[ci.ZOrder]; _images.Add ( ci ) ; } items.ItemsSource = _images; } } If you look at the code, I created a CollectionImage which aggregates the stuff from metadata.xml and the corresponding MultiScaleSubImage. This means I could now filter, or do any thing since the data is merged. Kirupa has an example of using tags to filter (so I will stop here on that topic and move to Viewports).
void wc_DownloadStringCompleted(objectsender, DownloadStringCompletedEventArgs e) { if(e.Cancelled == false&& e.Error == null) { strings = e.Result; XDocument doc = XDocument.Parse(s); var images = froma indoc.Element("Metadata").Descendants("Image") selecta; foreach ( XElement image inimages ) { CollectionImage ci = newCollectionImage { Height = (double) image.Element("Height"), Width = (double) image.Element("Width"),
//here we read the ZOrder from metadata.xml and subtract one ZOrder = ((int) image.Element("ZOrder")) -1 , Tag = (string) image.Element("Tag"), Location = newPoint{ X = (double)image.Element("x"), Y = (double) image.Element("y")} } ;
//here we map from the SubImages collection based on the ZOrder we read ci.Image = msi.SubImages[ci.ZOrder]; _images.Add ( ci ) ; } items.ItemsSource = _images; } }
If you look at the code, I created a CollectionImage which aggregates the stuff from metadata.xml and the corresponding MultiScaleSubImage. This means I could now filter, or do any thing since the data is merged. Kirupa has an example of using tags to filter (so I will stop here on that topic and move to Viewports).
ViewportOrigin:
ViewportOrigin represents the left(x), top(y) corner of your SubImage relative to the MultiScaleImage control. The surprise (for me at least) is that:
Got it?? OK! you are done. If you are like me you might want to see a sample. Here are some below:
Again, you can play with the ugly but hopefully useful sample here.. Just change the ViewportOrigin, or any other property and see what happens. You can use the same sample to play with Opacity, ZIndex and ViewportWidth.. this will show you the flexibility in collections. Don't get tricky with the values as there is no validation.
Mapping SubImages to Element Coordinates Now that we understand ViewportWidth and ViewportOrigin, we can map from logical coordinates to element coordinates so we can put overlays on our MultiScaleImage. Or do hit testing or any thing similar.
What I did is put a small pink rectangle in the page and I am going to listen to MouseMove on the MultiScaleImage and then do kind of a "hit testing" to see which Image I am over. I used ZIndex to select only the single image on the front. If you did not use ZIndex you can select multiple. So, what does the map look like?? The whole code is below commented in detail.. I hope that makes it easiest to explain -instead of my rambles-.
/// <summary> /// Gets a rectangle representing the top-most image that the mouse is over /// </summary> /// <param name="elementPoint">Mouse Position, in Element Coordinates</param> /// <returns> Rectangle reprsenting Element Coordinates for the image or 0,0,0,0 if not over an image</returns> Rect SubImageHitTestUsingElement(Point elementPoint) { Rect resultRect = new Rect(0, 0, 0, 0); int zIndex = 0; // We loop through all our images. for (int i = 0; i < _images.Count; i++) { try { // Selct our MSSI. MultiScaleSubImage subImage = _images[i].Image; // NOTICE the scale is a mutliplication of the size of our image (1 / subImage.ViewportWidth) // and the current Zoom level ( 1 / msi.ViewportWidth) double scaleBy = 1 / subImage.ViewportWidth * 1 / msi.ViewportWidth; // The two lines below convert our image size us from Logical to Element Coords // Notice that for Height, we must take into account Aspect ratio. double width = scaleBy * this.msi.ActualWidth; double height = (scaleBy * this.msi.ActualWidth * (1 / subImage.AspectRatio)); // Now we convert our viewportorigin (logical coords) to Element Coords // Reminder, this is top-left .. Notice that we multiply by -1 since // we saw the negative math for Viewport Origin. Point p = msi.LogicalToElementPoint( new Point( -subImage.ViewportOrigin.X * 1 / subImage.ViewportWidth, - subImage.ViewportOrigin.Y * 1 / subImage.ViewportWidth)); // Now we create a Rectangle in Element Coords. Rect subImageRect = new Rect(p.X, p.Y, width, height); // here we hitTest, using Contains.. // and we keep track of the front-most element only.. if (subImageRect.Contains(elementPoint)) { if (subImage.ZIndex >= zIndex) { zIndex = subImage.ZIndex; resultRect = subImageRect; } } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); } } System.Diagnostics.Debug.WriteLine("Done"); return resultRect; }
I used Element Coords because that is what I was after. If you want logical coords, it should be easy from code above.. Just convert the point to Logical, do the scaling for zoom and hittest against a logical rect... Fair enough??? The source is [you guessed it] at Skydrive.
You can see a few tiny issues I did not care much for: 1) My math is rounded so some times you see the 'Rectangle' I created be slightly off (adding some extra pixels should do fine) ... 2) I did the recalculation for rectangle only on mouse move.. and I did not do it on Pan... so if you Zoom using Wheel or you pan, it will take for you to move the mouse one more time in order for Rectangle overlay to update.
That is my part!! Now it is up to you to build some thing really cool using real images and hittesting..