Wednesday, February 9, 2011

Custom Magnifier for Esri’s Silverlight SDK

Custom Magnifier for Esri’s Silverlight SDK

Map magnifiers are very intuitive and compelling for end users. They allow a user to examine a map in more detail without having to to change the map scale (or “zoom in”). Magnifiers can also be used to like an “x-ray” to reveal alternative content. For example, if a base map shows a street map information for a city, a magnifier could reveal satellite imagery at the same scale.

The ArcGIS Silverlight Toolkit extends ESRI’s ArcGIS API for Microsoft Silverlight with useful widgets like a navigation control, toolbar and two types of magnifiers.

The toolkit’s Magnifier is a control that allows the user to temporarily swipe a stylized magnifying glass over the map. Developers can specify any magnification and any map content for the magnifier.

Magnifier

The toolkit’s MagnifyingGlass is an alternative magnifier with a simpler design. Developers can specify any magnification but are limited to single tiled map service layer for the contents. Unlike the Magnifier described above, this control can resided in the web application permanently. As the base map updates, the extent in the MagnifyingGlass will also update.

MagnifyingGlass

These two controls are very useful but did not satisfy my requirement to have permanent magnifier that supported dynamic layers such as an image service layer. The code below is a blend of both toolkit magnifiers.

Here is the XAML:

<UserControl
   x:Class="TestMagnify.Magnify"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   xmlns:esri="http://schemas.esri.com/arcgis/client/2009"
   mc:Ignorable="d"
   d:DesignHeight="225"
   d:DesignWidth="225"
   Width="225"
   Height="225"
   >
    <Grid x:Name="LayoutRoot">
        <Grid.RenderTransform>
            <TranslateTransform x:Name="Translate" />
        </Grid.RenderTransform>
        <Ellipse x:Name="MagShadow" Margin="-2" Fill="Black">
            <Ellipse.Effect>
                <BlurEffect Radius="25"/>
            </Ellipse.Effect>
        </Ellipse>
        <Ellipse x:Name="MagFrameBack" Margin="0">
            <Ellipse.Fill>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#FF000000" Offset="1"/>
                    <GradientStop Color="#FFFFFFFF" Offset="0"/>
                    <GradientStop Color="#FF353535" Offset="0.875"/>
                    <GradientStop Color="#FF515151" Offset="0.21"/>
                </LinearGradientBrush>
            </Ellipse.Fill>
        </Ellipse>
        <Ellipse x:Name="MagFrameFront" Margin="5">
            <Ellipse.Fill>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#FF000000"/>
                    <GradientStop Color="#FFFFFFFF" Offset="1"/>
                    <GradientStop Color="#FF505050" Offset="0.134"/>
                    <GradientStop Color="#FE787878" Offset="0.728"/>
                    <GradientStop Color="#FE9D9D9D" Offset="0.915"/>
                </LinearGradientBrush>
            </Ellipse.Fill>
        </Ellipse>
        <Grid Margin="10">
            <Grid.OpacityMask>
                <RadialGradientBrush GradientOrigin="0.5,0.5"
Center="0.5,0.5">
                    <GradientStop Color="White" Offset="0.9999"/>
                    <GradientStop Offset="1"/>
                </RadialGradientBrush>
            </Grid.OpacityMask>
            <esri:Map x:Name="MagMap"
                     IsLogoVisible="False"
                     IsHitTestVisible="False"
                     PanDuration="00:00:00"
                     ZoomDuration="00:00:00"/>
        </Grid>
        <Ellipse x:Name="MagGlass" Stroke="#FF000000" Margin="10">
            <Ellipse.Fill>
                <RadialGradientBrush GradientOrigin="1, 1" Center="0.5,0.5">
                    <GradientStop Color="#48FFFFFF" Offset="0.009"/>
                    <GradientStop Color="#22FFFFFF" Offset="0.107"/>
                    <GradientStop Color="#00BBBBBB" Offset="0.567"/>
                    <GradientStop Color="#00BCBCBC" Offset="0.585"/>
                    <GradientStop Color="#00BBBBBB" Offset="0.625"/>
                    <GradientStop Color="#04C4C4C4" Offset="0.696"/>
                    <GradientStop Color="#1AC4C4C4" Offset="0.888"/>
                    <GradientStop Color="#2FFFFFFF" Offset="1"/>
                </RadialGradientBrush>
            </Ellipse.Fill>
        </Ellipse>
    </Grid>
</UserControl>

Here is the code behind:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using ESRI.ArcGIS.Client;
using ESRI.ArcGIS.Client.Geometry;

namespace TestMagnify {
    public partial class Magnify : UserControl {
        private Point _begin;
        private Point _current;
        private Cursor _cursor = null;
        private bool _isdrag = false;
        //
        public Magnify() {
            InitializeComponent();
            this.MouseLeftButtonDown += this.MagnifyBox_MouseLeftButtonDown;
            this.MouseLeftButtonUp += this.MagnifyBox_MouseLeftButtonUp;
            this.MouseMove += this.MagnifyBox_MouseMove;
        }
        public static readonly DependencyProperty MapProperty =
            DependencyProperty.Register(
                "Map",
                typeof(Map),
                typeof(Magnify),
                new PropertyMetadata(Magnify.OnMapPropertyChanged));
        public static readonly DependencyProperty LayersProperty =
            DependencyProperty.RegisterAttached(
                "Layers",
                typeof(LayerCollection),
                typeof(Magnify),
                new PropertyMetadata(Magnify.OnLayersPropertyChanged));
        public static readonly DependencyProperty ZoomFactorProperty =
            DependencyProperty.Register(
                "ZoomFactor",
                typeof(double),
                typeof(Magnify),
                new PropertyMetadata(2d,
Magnify.OnZoomFactorPropertyChanged));
        public Map Map {
            get { return (Map)this.GetValue(MapProperty); }
            set { this.SetValue(MapProperty, value); }
        }
        public LayerCollection Layers {
            get { return (LayerCollection)GetValue(LayersProperty); }
            set { SetValue(LayersProperty, value); }
        }
        public double ZoomFactor {
            get { return (double)this.GetValue(ZoomFactorProperty); }
            set { this.SetValue(ZoomFactorProperty, value); }
        }
        private static void OnLayersPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
            Magnify magnify = d as Magnify;
            if (magnify.MagMap != null) {
                magnify.MagMap.Layers = e.NewValue as LayerCollection;
            }
        }
        private static void OnMapPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
            Magnify glass = d as Magnify;
            Map mapOld = e.OldValue as Map;
            if (mapOld != null) {
                mapOld.ExtentChanged -= glass.Map_ExtentChanged;
            }
            Map mapNew = e.NewValue as Map;
            if (mapNew != null) {
                mapNew.ExtentChanged += glass.Map_ExtentChanged;
            }
        }
        private static void OnZoomFactorPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
            (d as Magnify).UpdateMagnifier();
        }
        private void Map_ExtentChanged(object sender, ExtentEventArgs e) {
            this.UpdateMagnifier();
        }
        private void MagnifyBox_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e) {
            this._isdrag = true;
            this._cursor = this.Cursor;
            this._begin = e.GetPosition(null);
            this.Cursor = Cursors.None;
            this.CaptureMouse();
        }
        private void MagnifyBox_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e) {
            if (this._isdrag) {
                this._isdrag = false;
                this.ReleaseMouseCapture();
                this.UpdateMagnifier();
            }
            this.Cursor = this._cursor;
        }
        private void MagnifyBox_MouseMove(object sender, MouseEventArgs e) {
            if (this._isdrag) {
                this._current = e.GetPosition(null);
                double x = this._current.X - this._begin.X;
                double y = this._current.Y - this._begin.Y;
                if (this.FlowDirection == FlowDirection.RightToLeft) {
                    x *= -1d;
                }
                this.Translate.X += x;
                this.Translate.Y += y;
                this._begin = this._current;
            }
        }
        private void UpdateMagnifier() {
            if (this.Visibility == Visibility.Collapsed) { return; }
            if (this.Map == null) { return; }
            Point point = this.TransformToVisual(this.Map).Transform(
                new Point(
                    this.RenderSize.Width * 0.5d + this.Translate.X,
                    this.RenderSize.Height * 0.5d + this.Translate.Y
                )
            );
            MapPoint center = this.Map.ScreenToMap(point);
            double resolution = this.Map.Resolution;
            double zoomResolution = resolution / this.ZoomFactor;
            double width = 0.5d * this.MagMap.ActualWidth * zoomResolution;
            Envelope envelope = new Envelope() {
                XMin = center.X - width,
                YMin = center.Y - width,
                XMax = center.X + width,
                YMax = center.Y + width,
                SpatialReference = this.MagMap.SpatialReference
            };
            this.MagMap.Extent = envelope;
        }
    }
}

Here is how to use it:

<UserControl
   x:Class="TestMagnify.MainPage"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   xmlns:esri="http://schemas.esri.com/arcgis/client/2009"
   xmlns:local="clr-namespace:TestMagnify"
   mc:Ignorable="d"
   d:DesignHeight="300" d:DesignWidth="400">
    <Grid x:Name="LayoutRoot" Background="White">
        <esri:Map x:Name="Map">
            <esri:ArcGISImageServiceLayer
               Url="http://sampleserver3.arcgisonline.com ~
/ArcGIS/rest/services/Portland/Aerial/ImageServer"

               ImageFormat="JPGPNG" />
        </esri:Map>
        <local:Magnify x:Name="Magnify"
                      Map="{Binding ElementName=Map}"
                      ZoomFactor="5">
            <local:Magnify.Layers>
                <esri:LayerCollection>
                    <esri:ArcGISImageServiceLayer
                       Url="http://sampleserver3.arcgisonline.com ~
/ArcGIS/rest/services/Portland/Aerial/ImageServer"

                       ImageFormat="JPGPNG" />
                </esri:LayerCollection>
            </local:Magnify.Layers>
        </local:Magnify>
    </Grid>
</UserControl>

Note: “~” denotes an editorial line continuation.

In conclusion, based on source code extracted from Esri’s ArcGIS Silverlight toolkit project page on codeplex, I was able to create a new magnifier that could support any layer or layers regardless of type. In order to support dynamic layers such an image service layers the new magnifier had to reserve updates until after a drag operation is completed. This is not ideal but unavoidable.

Using this code I hope you can explore some innovative variants like “x-ray” controls. Fun and productive controls like these are being increasingly important as web apps are becoming more tactile with touch support in new hardware like this.

Wednesday, February 2, 2011

How to preserve the map center as the browser resizes

By default, when a Silverlight map (or its parent) is resized the map scale (or resolution) is preserved but not the map center. Instead, the map is “pinned” to the upper left hand corner.

In most situations this is desirable but occasionally it is not. The following code snippet will ensure that the current map center (and scale) is preserved as the browser is resized.

using System;
using System.Windows;
using System.Windows.Controls;
using ESRI.ArcGIS.Client.Geometry;

namespace SilverlightApplication2 {
    public partial class MainPage : UserControl {
        public MainPage() {
            InitializeComponent();

            this.SizeChanged += (s, e) => {
                if (e.PreviousSize.Height == 0 ||
e.PreviousSize.Width == 0) { return; }
                Point p = new Point() {
                    X = (this.MyMap.ActualWidth / 2d) -
(e.NewSize.Width - e.PreviousSize.Width),
                    Y = (this.MyMap.ActualHeight / 2d) -
(e.NewSize.Height - e.PreviousSize.Height)
                };
                MapPoint m = this.MyMap.ScreenToMap(p);
                TimeSpan t = this.MyMap.PanDuration;
                this.MyMap.PanDuration = TimeSpan.Zero;
                this.MyMap.PanTo(m);
                this.MyMap.PanDuration = t;
            };
        }
    }
}