Lens Magnifier and Flip Animation


Flip Animations
Fig 1 – Using ProjectionPlane for flip animations

Magnifiers are an interesting tool for some UIs. They function as a small area blowup of a larger Canvas. Here is a Magnifier example that Matt Serbinski put up on codeplex.

Flip Animations
Fig 2 – Sample Magnify Tool

I decided to look at approaches to making magnifier tools for Map controls. There are a couple of ways available for magnifying a Map Control in Silverlight. The easiest is to simply increase the LocalOffsetZ value of the canvas or UIControl. The effect is to bring the Canvas closer in the viewport by moving it in the positive z direction of the PlaneProjection axis. This does magnify the current zoom level, but adds no new detail. As the LocalOffsetZ increases the features eventually become blurry.

In cases where the layers are MapTileLayer.TileSources, a better approach is to actually move the magnifier through the tile source pyramid. This provides additional detail at each zoom level. To make use of this approach a Lens needs to replicate a set of the main map tile layer sources. Here is an example of a KeyMap populated with three tile sources, OSM, TopOSM, and TopOSMContours, matching the layers of the Main Map Canvas.

  <Canvas>
    <m:Map x:Name="KeyMap"
        Canvas.Left="-100" Canvas.Top="-50"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"
        AnimationLevel="Full"
        ScaleVisibility="Collapsed"
        LogoVisibility="Collapsed"
        NavigationVisibility="Collapsed"
        >

      <m:Map.Mode>
        <local:EmptyMapMode/>
      </m:Map.Mode>

      <m:Map.Children>
        <m:MapTileLayer Opacity="1" x:Name="OSM" Visibility="Visible">
          <m:MapTileLayer.TileSources>
            <local:OpenStreetMapTileSource></local:OpenStreetMapTileSource>
          </m:MapTileLayer.TileSources>
        </m:MapTileLayer>

        <m:MapTileLayer x:Name="TopOSMColor" Visibility="Collapsed">
          <m:MapTileLayer.TileSources>
            <local:TopOSMColorTileSource></local:TopOSMColorTileSource>
          </m:MapTileLayer.TileSources>
        </m:MapTileLayer>

        <m:MapTileLayer x:Name="TopOSMContours" Visibility="Collapsed">
          <m:MapTileLayer.TileSources>
            <local:TopOSMContoursTileSource></local:TopOSMContoursTileSource>
          </m:MapTileLayer.TileSources>
        </m:MapTileLayer>
      </m:Map.Children>
    </m:Map>

    <Button x:Name="Flip" Content="Key" Width="40" Height="20"
        Canvas.Left="80" Canvas.Top="170" />
    <Ellipse Stroke="Black" StrokeThickness="5" Width="200" Height="200"/>
  </Canvas>

In this example I have created two additional UserControls a LensPanel and a KeyPanel both like the one above with three tile source layers. These are then added to my MainPage.xaml:

<Canvas Grid.Column="0" Grid.Row="0" Grid.RowSpan="2"
    HorizontalAlignment="Left" VerticalAlignment="Top" >
  <local:DragDropPanel x:Name="KeyDragPanel">
    <Grid x:Name="KeyMapGrid" CacheMode="BitmapCache">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="200" />
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="200"/>
      </Grid.RowDefinitions>
      <Grid.Clip>
        <EllipseGeometry Center="100,100" RadiusX="100" RadiusY="100" />
      </Grid.Clip>
      <Grid.Projection>
        <PlaneProjection x:Name="FlipAnimate" RotationY="0" />
      </Grid.Projection>
      <local:Lens x:Name="LensPanel" Visibility="Visible"></local:Lens>
      <local:Key x:Name="KeyPanel" Visibility="Collapsed"></local:Key>
    </Grid>
  </local:DragDropPanel>
</Canvas>

As you can see from the above Canvas, these additional panels sit inside a 200 x 200 Grid element and make use of an EllipseGeometry Clip mask to limit the view to a circular lens area. In addition this Grid is wrapped in a DragDropPanel so that it can be moved with mouse events against the backdrop of the MainMap. At initialization a LayoutUpdated event handler is added to the KeyDragPanel:
    KeyDragPanel.LayoutUpdated += KeyChange;

Here is the KeyChange event handler:

  private void KeyChange(object sender, EventArgs e)
  {
    if (KeyDragPanel.dragOn)
    {
      loc = MainMap.ViewportPointToLocation(KeyDragPanel.currentP);
      LensPanel.LensMap.View = new MapViewSpecification(loc, MainMap.View.ZoomLevel + lensZoom);
    }
  }

It takes the MainMap current mouse position as a Lat,Lon Location and synchronizes the LensMap.View to the that same Location. However, instead of simply synchronizing ZoomLevels the Lens ZoomLevel is increased by a lensZoom factor, which is initially set to 2. The lens magnification can be adjusted by changing this lensZoom factor with +/- buttons or MouseWheel events. Now the Lens is viewing the same location as the MainMap, but at a deeper more detailed level in the tile pyramid. With this relatively simple addition to a normal Map client we have a functional magnifier lens, which a user can drag around the map, showing tile source layers at a higher zoomlevel out of the source pyramid.

If you notice what happens when a lensZoom factor is subtracted rather than added, you have in essence a basic Key map. The focal length of the magnifier is reversed. Just to make this a little more interesting I have added both a lens and a key UserControl, but instead of making these two separate controls I decided to spice up the sample by making the lens into a flip animation. On one side is the magnifying lens and on the other side is the key map.


Flip Animations
Fig 3 – Key Map on the reverse side of a Lens Magnifier

Flip animations are made possible by targeting a ProjectionPlane Rotation from a Storyboard animation:

  <UserControl.Resources>
    <Storyboard x:Name="stb" Completed="stb_Completed" >
      <DoubleAnimation Storyboard.TargetName="FlipAnimate"
               Storyboard.TargetProperty="RotationY"
               Duration="0:0:1" From="0" To="90" />
    </Storyboard>
    <Storyboard x:Name="stbfinish" >
      <DoubleAnimation Storyboard.TargetName="FlipAnimate"
               Storyboard.TargetProperty="RotationY"
               Duration="0:0:1" From="-90" To="0" />
    </Storyboard>
  </UserControl.Resources>

Actually two Storyboards, one for the front side first 90 degrees, and a second for the backside last 90 degrees. As the front Storyboard completes it triggers a switch from the front user control to the back. In this case from the LensPanel to the KeyPanel. Here is the Completed event handler:

  private void stb_Completed(object sender, EventArgs e)
  {
    lensZoom = 2;
    if (LensPanel.Visibility == Visibility.Visible)
    {
        LensPanel.Visibility = Visibility.Collapsed;
        KeyPanel.Visibility = Visibility.Visible;
    }
    else
    {
        LensPanel.Visibility = Visibility.Visible;
        KeyPanel.Visibility = Visibility.Collapsed;
    }
    stbfinish.Begin();
  }

Rather than using a Clear and Add for switching User Controls between Lens and Key, this event handler just toggles Visibility. Keeping both User Controls in the MainPage and switching with Visibility doesn’t add much to downloads while making the switch faster. After toggling Visibility the Completed event handler then finishes the flip through the last 90 degrees by triggering the stbfinish Storyboard Begin() method. The switch happens at 90 degrees because that is the angle of RotationX when the view is edge on, and the panel disappears momentarily.

Here is the Flip Animation sequence:

  1. start the initial storyboard stb.Begin();
  2. at the end of 90 degrees the Completed event handler is triggered
  3. after toggling controls the Completed event triggers the final soryboard stbfinish.Begin()
  4. finally the RotationX is completed and the flip animation is done.

Summary

Using PlaneProjection for a flip animation of the Lens and KeyMap tools is an interesting UI enhancement. Although this works very well for tile pyramid sources, it does have one drawback: It is limited to tile source layers. WMS rectangle sources require a new GetMap request with each change, which means that magnification of WMS layers will not occur until the lens stops moving. WMS GetMap requests are just not fast enough to keep up with a Lens drag. In the case of a set of WMS layers it is probably better to work out a LocalOffsetZ magnifier.

The performance and user experience improvements are so great with tile pyramid sources that I assume going forward these will be the services of choice. Tile pyramids are not hard to create and in the case of GeoServer geowebcache builds the pyramids for you. With the cost of disk space and servers falling all the time there really doesn’t seem to be much of a reason to continue using the slower WMS GetMap request for relatively static layers.

At the same time Silverlight vector performance has improved significantly over javascript. This means that dynamic geospatial sources might be better sourced as vector features either directly from the database or decoupled with WFS services. Unlike WMS services vector magnification can happen indefintely in the client.

Web service architectures will likely evolve toward static tile pyramids and dynamic vector layers. However, there will be older important WMS services for some time to come. It would be nice to see some of the federal web services add tile pyramid endpoints in the coming year. Maybe this is a good opportunity for using stimulus funding at USGS, MRLC, NASA, JPL, Census, NOAA ….. Cloud resources such as Amazon AWS and Microsoft Azure make high capacity public tile pyramids feasible even for smaller agencies.

Comments are closed.