Blograby

Deep Zoom Viewer (Pinch, Stretch, & Double Tap Gestures)

Deep Zoom is a slick technology for creating, viewing, and manipulating huge images or collections of images. It can be used to create experiences much like Bing Maps or Google Maps, but applied to any domain. With the samples available from this app, you can explore large panoramic photographs, scanned-in artwork, a computer-generated data visualization, an example of what a deep zoom advertisement might look like, and, yes, Earth.

To maximize performance, Deep Zoom images are multiresolution; the image file format includes many separate subimages—called tiles—at multiple zoom levels. Tiles are downloaded on-demand and rendered in a fairly seamless fashion with smooth transitions. For end users, the result is a huge image that can be loaded, zoomed, and panned extremely quickly.

Deep Zoom Viewer enables viewing and interacting with any online Deep Zoom image right on your Windows phone. You can enter a URL that points to any Deep Zoom image (or image collection), or you can browse any of the seven interesting samples that are already provided.

To render a Deep Zoom image, this app leverages Silverlight’s MultiScaleImage control, which does all the hard work. To view a file, you just need to place a MultiScaleImage on a page and then set its Source property to an appropriate URL. However, the control does not provide any built-in gestures for manipulating the image. Therefore, this app provides a perfect opportunity to demonstrate how to implement pinch-&-stretch zooming and double-tap gestures—practically a requirement for any respectable Deep Zoom viewer.

Pinching is the standard zoom-out gesture that involves placing two fingers on the screen and then sliding them toward each other. Stretching is the standard zoom-in gesture that involves placing two fingers on the screen and then sliding them away from each other. In this app, double tapping is used to quickly zoom in, centered on the point that was tapped.

Windows Phone style guidelines dictate that touch gestures should only be used for their intended purposes. As in Deep Zoom Viewer, a pinch should always zoom out, a stretch should always zoom in, and a double tap should always perform some kind of zoom in and/or zoom out.

How do I create my own Deep Zoom images?

Currently, the quickest and easiest way is to use Microsoft’s free Zoom.it service (http://zoom.it).This turns any JPG, PNG, or TIFF image into a Deep Zoom image.The service also supports SVG files, PDF files, and even web pages as input! You just enter an appropriate URL, and it does the conversion. It even hosts the file for you! Alternatively, the most powerful option is to use Microsoft’s Deep Zoom Composer, a free program that can be downloaded at http://bit.ly/deepzoomdownload.

Deep Zoom versus Seadragon

Although Deep Zoom refers to a Silverlight-specific feature, the underlying Seadragon technology (which Microsoft originally acquired from a company called Seadragon Software) has been exposed in other forms. For example,Microsoft has released an open-source JavaScript version called “Seadragon Ajax” in its Ajax Control Toolkit. It can view the same file types as Deep Zoom.

The User Interface

Deep Zoom Viewer is a single-page app (except for an instructions page) that dedicates all of its screen real estate to the MultiScaleImage control. On top of this, it layers a translucent application bar and a dialog that enables the user to type arbitrary Deep Zoom image URLs. Figure 41.1 shows the main page with its application bar menu expanded, and Figure 41.2 shows the main page with its dialog showing. The XAML for this page is in Listing 41.1.

FIGURE 41.1 The application bar menu is expanded on top of the Carina Nebula.
FIGURE 41.2 Entering a custom URL is done via a dialog that appears on top of the current Deep Zoom image.

LISTING 41.1 MainPage.xaml—The User Interface for Deep Zoom Viewers’Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<!– The application bar –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5”>
<shell:ApplicationBarIconButton Text=”fit to screen”
IconUri=”/Images/appbar.fitToScreen.png”
Click=”FitToScreenButton_Click”/>
<shell:ApplicationBarIconButton Text=”zoom in”
IconUri=”/Shared/Images/appbar.plus.png”
Click=”ZoomInButton_Click”/>
<shell:ApplicationBarIconButton Text=”zoom out”
IconUri=”/Shared/Images/appbar.minus.png”
Click=”ZoomOutButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”[enter url]”
Click=”CustomUrlMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– The Deep Zoom image –>
<MultiScaleImage x:Name=”DeepZoomImage”>
<!– Attach the gesture listener to this element –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener DoubleTap=”GestureListener_DoubleTap”
PinchStarted=”GestureListener_PinchStarted”
PinchDelta=”GestureListener_PinchDelta”/>
</toolkit:GestureService.GestureListener>
</MultiScaleImage>
<!– Show a progress bar while loading an image –>
<ProgressBar x:Name=”ProgressBar” Visibility=”Collapsed”/>
<!– A dialog for entering a URL –>
<local:Dialog x:Name=”CustomFileDialog” Closed=”CustomFileDialog_Closed”>
<local:Dialog.InnerContent>
<StackPanel>
<TextBlock Text=”Enter the URL of a Deep Zoom file” Margin=”11,5,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBox InputScope=”Url” Text=”{Binding Result, Mode=TwoWay}”/>
</StackPanel>
</local:Dialog.InnerContent>
</local:Dialog>
</Grid>
</phone:PhoneApplicationPage>

[/code]

FIGURE 41.3 You can occasionally catch pieces of the view starting out blurry and then seamlessly becoming crisp.

The Code-Behind

Listing 41.2 contains the code-behind for the main page.

LISTING 41.2 MainPage.xaml.cs—The Code-Behind for Deep Zoom Viewers’Main Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Persistent settings
Setting<Uri> savedImageUri = new Setting<Uri>(“ImageUri”,
new Uri(Data.BaseUri, “last-fm.dzi”));
Setting<Point> savedViewportOrigin = new Setting<Point>(“ViewportOrigin”,
new Point(0, -.2));
Setting<double> savedZoom = new Setting<double>(“Zoom”, 1);
// Used by pinch and stretch
double zoomWhenPinchStarted;
// Used by panning and double-tapping
Point mouseDownPoint = new Point();
Point mouseDownViewportOrigin = new Point();
public MainPage()
{
InitializeComponent();
// Fill the application bar menu with the sample images
foreach (File f in Data.Files)
{
ApplicationBarMenuItem item = new ApplicationBarMenuItem(f.Title);
// This assignment is needed so each anonymous method gets the right value
string filename = f.Filename;
item.Click += delegate(object sender, EventArgs e)
{
OpenFile(new Uri(Data.BaseUri, filename), true);
};
this.ApplicationBar.MenuItems.Add(item);
}
// Handle success for any attempt to open a Deep Zoom image
this.DeepZoomImage.ImageOpenSucceeded +=
delegate(object sender, RoutedEventArgs e)
{
// Hide the progress bar
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false; // Avoid a perf issue
// Initialize the view
this.DeepZoomImage.ViewportWidth = this.savedZoom.Value;
this.DeepZoomImage.ViewportOrigin = this.savedViewportOrigin.Value;
};
// Handle failure for any attempt to open a Deep Zoom image
this.DeepZoomImage.ImageOpenFailed +=
delegate(object sender, ExceptionRoutedEventArgs e)
{
// Hide the progress bar
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false; // Avoid a perf issue
MessageBox.Show(“Unable to open “ + this.savedImageUri.Value + “.”,
“Error”, MessageBoxButton.OK);
};
// Load the previously-viewed (or default) image
OpenFile(this.savedImageUri.Value, false);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Remember settings for next time
this.savedViewportOrigin.Value = this.DeepZoomImage.ViewportOrigin;
this.savedZoom.Value = this.DeepZoomImage.ViewportWidth;
}
// Attempt to open the Deep Zoom image at the specified URI
void OpenFile(Uri uri, bool resetPosition)
{
if (resetPosition)
{
// Restore these settings to their default values
this.savedZoom.Value = this.savedZoom.DefaultValue;
this.savedViewportOrigin.Value = this.savedViewportOrigin.DefaultValue;
}
this.savedImageUri.Value = uri;
// Assign the image
this.DeepZoomImage.Source = new DeepZoomImageTileSource(uri);
// Show a temporary progress bar
this.ProgressBar.IsIndeterminate = true;
this.ProgressBar.Visibility = Visibility.Visible;
}
// Three handlers (mouse down/move/up) to implement panning
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
this.mouseDownPoint = e.GetPosition(this.DeepZoomImage);
this.mouseDownViewportOrigin = this.DeepZoomImage.ViewportOrigin;
this.DeepZoomImage.CaptureMouse();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
Point p = e.GetPosition(this.DeepZoomImage);
// ViewportWidth is the absolute zoom (2 == half size, .5 == double size)
double scale = this.DeepZoomImage.ActualWidth /
this.DeepZoomImage.ViewportWidth;
// Pan the image by setting a new viewport origin based on the mouse-down
// location and the distance the primary finger has moved
this.DeepZoomImage.ViewportOrigin = new Point(
this.mouseDownViewportOrigin.X + (this.mouseDownPoint.X – p.X) / scale,
this.mouseDownViewportOrigin.Y + (this.mouseDownPoint.Y – p.Y) / scale);
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// Stop panning
this.DeepZoomImage.ReleaseMouseCapture();
}
// The three gesture handlers for double tap, pinch, and stretch
void GestureListener_DoubleTap(object sender, GestureEventArgs e)
{
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
// Zoom in by a factor of 2 centered at the place where the double tap
// occurred (the same place as the most recent MouseLeftButtonDown event)
ZoomBy(2, this.mouseDownPoint);
}
// Raised when two fingers touch the screen (likely to begin a pinch/stretch)
void GestureListener_PinchStarted(object sender,
PinchStartedGestureEventArgs e)
{
this.zoomWhenPinchStarted = this.DeepZoomImage.ViewportWidth;
}
// Raised continually as either or both fingers move
void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
// The distance ratio is always relative to when the pinch/stretch started,
// so be sure to apply it to the ORIGINAL zoom level, not the CURRENT
double zoom = this.zoomWhenPinchStarted / e.DistanceRatio;
this.DeepZoomImage.ViewportWidth = zoom;
}
void ZoomBy(double zoomFactor, Point centerPoint)
{
// Restrict how small the image can get (don’t get smaller than half size)
if (this.DeepZoomImage.ViewportWidth >= 2 && zoomFactor < 1)
return;
// Convert the on-screen point to the image’s coordinate system, which
// is (0,0) in the top-left corner and (1,1) in the bottom right corner
Point logicalCenterPoint =
this.DeepZoomImage.ElementToLogicalPoint(centerPoint);
// Perform the zoom
this.DeepZoomImage.ZoomAboutLogicalPoint(
zoomFactor, logicalCenterPoint.X, logicalCenterPoint.Y);
}
// Code for the custom file dialog
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If the dialog is open, close it instead of leaving the page
if (this.CustomFileDialog.Visibility == Visibility.Visible)
{
e.Cancel = true;
this.CustomFileDialog.Hide(MessageBoxResult.Cancel);
}
}
void CustomFileDialog_Closed(object sender, MessageBoxResultEventArgs e)
{
// Try to open the typed-in URL
if (e.Result == MessageBoxResult.OK && this.CustomFileDialog.Result != null)
OpenFile(new Uri(this.CustomFileDialog.Result.ToString()), true);
}
// Application bar handlers
void FitToScreenButton_Click(object sender, EventArgs e)
{
this.DeepZoomImage.ViewportWidth = 1; // Un-zoom
this.DeepZoomImage.ViewportOrigin = new Point(0, -.4); // Give a top margin
}
void ZoomInButton_Click(object sender, EventArgs e)
{
// Zoom in by 50%, keeping the current center point
ZoomBy(1.5, new Point(this.DeepZoomImage.ActualWidth / 2,
this.DeepZoomImage.ActualHeight / 2));
}
void ZoomOutButton_Click(object sender, EventArgs e)
{
// Zoom out by 50%, keeping the current center point
ZoomBy(1 / 1.5, new Point(this.DeepZoomImage.ActualWidth / 2,
this.DeepZoomImage.ActualHeight / 2));
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/InstructionsPage.xaml”, UriKind.Relative));
}
void CustomUrlMenuItem_Click(object sender, EventArgs e)
{
// Show the custom file dialog, initialized with the current URI
if (this.savedImageUri.Value != null)
this.CustomFileDialog.Result = this.savedImageUri.Value;
this.CustomFileDialog.Show();
}
}
}

[/code]

Can MultiScaleImage work with a local image included with the app?

Surprisingly, no! Only online files are supported.

Be sure to use a logical point when setting ViewportOrigin!

Otherwise, the image will likely pan far offscreen. Luckily, MultiScaleImage provides two handy methods—ElementToLogicalPoint and LogicalToElementPoint—for converting between logical points and element-relative points. (When the MultiScaleImage control fills the screen and has no transforms applied, as in this app, element-relative points are equivalent to points on the screen.)

MultiScaleImage has built-in inertia effects whenever you change the zoom level or viewport origin, so the panning and zooming done by this app exhibit smooth and inertial transitions without any extra work. If you do not want these effects, simply set MultiScaleImage’s UseSprings property to false.

The same three gesture listener events—PinchStarted, PinchDelta, and PinchCompleted—can be used to detect both pinching and stretching.The key piece of data is the DistanceRatio property on PinchGestureEventArgs, which indicates how far apart or close together the two fingers are compared to when they first touched the screen. Be careful how you use this value, however. A value greater than 1 does not necessarily mean stretching is occurring, and a value less than one does not necessarily mean pinching is occurring. For example, users could stretch their fingers until the ratio is 5 and then pinch them until the ratio goes back down to 2. As long as the ratio is continually applied to the zooming element’s original zoom level when the pinch/stretch started rather than the current zoom level, pinching and stretching will work as intended.

How do I determine the center point of a pinch or stretch gesture, so I can center my zoom on that point?

Although it’s not done by this app (due to flakiness in constantly recentering the viewport), it’s common practice to center the zoom of a pinch or stretch gesture based on the midpoint between the two fingers. Although this point is not directly exposed by the gesture listener, you can calculate it as follows:

[code]

void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
Point firstPoint = e.GetPosition(this, 0); // Finger #1
Point secondPoint = e.GetPosition(this, 1); // Finger #2
// Calculate the midpoint
Point pinchOrigin = new Point(
(firstPoint.X + secondPoint.X) / 2,
(firstPoint.Y + secondPoint.Y) / 2);

}

[/code]

Both PinchGestureEventArgs and PinchStartedGestureEventArgs expose an overload of GetPosition that enables passing 0 or 1 to get the point for either of the two relevant fingers. (The regular GetPosition overload always gives the data for the first, primary finger.) By continually calculating the midpoint in a PinchDelta event handler rather than once in a PinchStarted event handler, the center is continually updated as the two fingers move,which gives the best experience.

The Finished Product

Exit mobile version