Chapter 41. Deep Zoom Viewer

image

image

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 multi-resolution; 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.

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.

image

Figure 41.2 Entering a custom URL is done via a dialog that appears on top of the current Deep Zoom image.

image

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


<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>


Notes:

→ A gesture listener from the Silverlight for Windows Phone Toolkit is attached to the MultiScaleImage control, so we can very easily detect double taps and pinch/stretch gestures.

→ The dialog for entering custom URLs is implemented with a Dialog user control that has been used by other apps in this book.

→ The MultiScaleImage control has a lot of automatic functionality to make the viewing experience as smooth as possible. For example, as tiles are downloaded, they are smoothly blended in with a blurry-to-crisp transition, captured in Figure 41.3.

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

image

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


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();
    }
  }
}


Notes:

→ The application bar menu is filled with a list of sample files based on the following two classes defined in a separate Data.cs file:

public struct File
{
  public string Title { get; set; }
  public string Filename { get; set; }
}

public static class Data
{
  public static readonly Uri BaseUri =
    new Uri("http://static.seadragon.com/content/misc/");

  public static File[] Files = {
    new File { Title = "World-Wide Music Scene", Filename = "last-fm.dzi" },
    new File { Title = "Carina Nebula", Filename = "carina-nebula.dzi" },
    new File { Title = "Blue Marble", Filename = "blue-marble.dzi" },
    new File { Title = "Contoso Fixster", Filename = "contoso-fixster.dzi" },
    new File { Title = "Milwaukee, 1898", Filename = "milwaukee.dzi" },
    new File { Title = "Yosemite Panorama", Filename="yosemite-panorama.dzi" },
    new File { Title = "Angkor Wat Temple", Filename = "angkor-wat.dzi" }
  };
}

→ When constructing the URI for each filename, BaseUri is prepended to the filename using an overloaded constructor of Uri that accepts two arguments.

→ Much like the Image element, the MultiScaleImage element is told what to render by setting its Source property. This is done inside OpenFile. Note that the type of Source is MultiScaleTileSource, an abstract class with one concrete subclass: DeepZoomImageTileSource.

→ After setting Source, the image download is asynchronous and either results in an ImageOpenSucceeded or ImageOpenFailed event being raised. This listing leverages this fact to temporarily show an indeterminate progress bar while the initial download is occurring, although this is usually extremely fast.

→ The current zoom level and visible region of the image are represented by two properties: ViewportWidth and ViewportOrigin.

ViewportWidth is actually the inverse of the zoom level. A value of .5 means that half the width is visible. (So the zoom level is 2.) A value of 2 means that the width of the viewport is double that of the image, so the image width occupies half of the visible area.

ViewportOrigin is the point in the image that is currently at the top-left corner of the visible area. The point is expressed in what Deep Zoom calls logical coordinates. In this system, (0,0) is the top-left corner of the image, and (1,1) is the bottom-right corner of the image.

→ This app’s panning functionality is supported with traditional MouseLeftButtonDown, MouseMove, and MouseLeftButtonUp handlers that implement a typical drag-and-drop scheme. In MouseMove, the amount that the finger has moved since MouseLeftButtonDown is applied to the ViewportOrigin, but this value is scaled appropriately based on the control’s width (480 or 800, depending on the phone orientation) and the zoom level. This is necessary because ViewportOrigin must be set to a logical point, and it also ensures that the panning gesture doesn’t get magnified as the user zooms in.

→ After the three handlers that implement panning, this listing contains the three handlers for gesture listener events. The first handler (GestureListener_DoubleTap) performs a 2x zoom each time a double tap is detected.

→ The next two handlers (GestureListener_PinchStarted and GestureListener_PinchDelta) handle pinching and stretching gestures. The DistanceRatio property reveals how much further apart (>1) or closer together (<1) the two fingers are, compared to when they made contact with the screen. The key to getting the appropriate effect is to apply this ratio to the original zoom level captured in the PinchStarted event handler. Normally, as with a ScaleTransform or CompositeTransform, you would multiply the original value by the ratio. Because ViewportWidth is the inverse of the zoom level, however, this listing instead divides its value by the ratio.

GestureListener_PinchDelta directly updates ViewportWidth rather than calling the ZoomBy method used elsewhere. ZoomBy centers the zoom around a passed-in point, but MultiScaleImage doesn’t work well when the viewport is continually and rapidly moved.

ZoomBy, used by the double-tap handler and the zooming application bar button handlers, zooms the viewport by an amount relative to the current zoom level with MultiScaleImage’s ZoomAboutLogicalPoint method.

The Finished Product

image

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.38.43