Quantcast
Channel: Category Name
Viewing all articles
Browse latest Browse all 10804

Part 2 – Bring your maps to life: Creating animations with Bing Maps (.NET)

$
0
0

In a recent blog post, you saw how to use JavaScript and CSS3 to create custom animations. Now you’ll see how to create the same custom animations for a Windows Store app using .NET.  You are in luck, as all of these animations can be used with the Bing Maps WPF control as well with just a few tweaks.

You can find the complete source code for this blog post in the MSDN code sample here.

Setting up the base project

To get started, open Visual Studio and create a new Windows Store project in C# or Visual Basic. Select the Blank App Template and call the project AnimatedMaps.

Screenshot: Add new project

Screenshot: Add new project

Add a reference to the Bing Maps SDK. To do this, right-click the References folder and press Add Reference. Select WindowsExtensions, and then select Bing Maps for C#, C++ and Visual Basic.

If you don’t see this option, make sure that you have installed the Bing Maps SDK for Windows Store apps. While you are here, add a reference to the Microsoft Visual C++ Runtime Package as this is required by the Bing Maps SDK.

Screenshot: Reference Manager

Screenshot: Reference Manager

If you notice that there is a little yellow indicator on the references that you just added. The reason for this is that in the C++ runtime package you have to set the Active solution platform in Visual Studio to one of the following options; ARM, x86 or x64. To do this, right click on the Solution folder and select Properties. Then go to Configuration Properties → Configuration. Find your project and under the Platform column set the target platform. For this blog post I’m going to select x86. Press Ok and the yellow indicator should disappear from our references.

Screenshot: Configuration Manager

Screenshot: Configuration Manager

Setting up the UI

The application will consist of a map and a panel that floats above the map that has a bunch of buttons for testing different animations. To do this, open the MainPage.xaml file and update the XAML to the following:

<Pagex:Class="AnimatedMaps.MainPage"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="using:AnimatedMaps"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:m="using:Bing.Maps"mc:Ignorable="d"><GridBackground="{ThemeResource ApplicationPageBackgroundThemeBrush}"><m:MapName="MyMap"Credentials="YOUR_BING_MAPS_KEY"/><GridWidth="270"Height="610"VerticalAlignment="Center"HorizontalAlignment="Right"Margin="10"><BorderBackground="Black"Opacity="0.8"CornerRadius="10"/><StackPanelMargin="10"><ButtonContent="Clear Map"Tapped="ClearMapBtn_Tapped"Height="45"Width="120"/><TextBlockText="Pushpin Animations"FontSize="16"FontWeight="Bold"Margin="0,10,0,0"/><ButtonContent="Scaling Pin"Tapped="ScalingPinBtn_Tapped"Height="45"Width="120"/><TextBlockText="Pushpin Animations"FontSize="16"FontWeight="Bold"Margin="0,10,0,0"/><ButtonContent="Drop Pin"Tapped="DropPinBtn_Tapped"Height="45"Width="120"/><ButtonContent="Bounce Pin"Tapped="BouncePinBtn_Tapped"Height="45"Width="120"/><ButtonContent="Bounce 4 Pins After Each Other"Tapped="Bounce4PinsBtn_Tapped"Height="45"Width="250"/><TextBlockText="Path Animations"FontSize="16"FontWeight="Bold"Margin="0,10,0,0"/><ButtonContent="Move Pin Along Path"Tapped="MovePinOnPathBtn_Tapped"Height="45"Width="250"/><ButtonContent="Move Pin Along Geodesic Path"Tapped="MovePinOnGeodesicPathBtn_Tapped"Height="45"Width="250"/><ButtonContent="Move Map Along Path"Tapped="MoveMapOnPathBtn_Tapped"Height="45"Width="250"/><ButtonContent="Move Map Along Geodesic Path"Tapped="MoveMapOnGeodesicPathBtn_Tapped"Height="45"Width="250"/><ButtonContent="Draw Path"Tapped="DrawPathBtn_Tapped"Height="45"Width="250"/><ButtonContent="Draw Geodesic Path"Tapped="DrawGeodesicPathBtn_Tapped"Height="45"Width="250"/>StackPanel>Grid>Grid>Page>

 

In the code behind for the MainPage.xaml we will need to add event handlers for the buttons. While we are at it we will add a couple of global variable and also add a MapShapeLayer for rendering shapes on the map which will be useful later. We will also add the logic to clear the map which will clear both the shape layer and map. If using C# update the MainPage.xaml.cs file with the following code:

using Bing.Maps;using System.Threading.Tasks;using Windows.UI;using Windows.UI.Xaml.Controls;using Windows.UI.Xaml.Input;using Windows.UI.Xaml.Media;using Windows.UI.Xaml.Media.Animation;namespace AnimatedMaps
{publicsealedpartialclass MainPage : Page
    {private MapShapeLayer shapeLayer;private LocationCollection path = new LocationCollection(){new Location(42.8, 12.49),   //Italynew Location(51.5, 0),       //Londonnew Location(40.8, -73.8),   //New Yorknew Location(47.6, -122.3)   //Seattle
        };public MainPage()
        {this.InitializeComponent();

            shapeLayer = new MapShapeLayer();
            MyMap.ShapeLayers.Add(shapeLayer);
        }privatevoid ClearMapBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
            ClearMap();
        }privatevoid DropPinBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }privatevoid BouncePinBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }private async void Bounce4PinsBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {   
        }privatevoid MovePinOnPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }privatevoid MovePinOnGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }privatevoid MoveMapOnPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }privatevoid MoveMapOnGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }privatevoid DrawPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }privatevoid DrawGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }privatevoid ClearMap()
        {
            MyMap.Children.Clear();
            shapeLayer.Shapes.Clear();
        }
    }
}

 

If using Visual Basic update the MainPage.xaml.vb file with the following code:

Imports Bing.MapsImports System.Threading.TasksImports Windows.UIImports Windows.UI.Xaml.ControlsImports Windows.UI.Xaml.InputImports Windows.UI.Xaml.Media.AnimationPublicNotInheritableClass MainPageInherits PagePrivate shapeLayer As MapShapeLayer'Italy'London'New York'SeattlePrivate path AsNew LocationCollection() From { _New Location(42.8, 12.49), _New Location(51.5, 0), _New Location(40.8, -73.8), _New Location(47.6, -122.3) _
    }PublicSubNew()Me.InitializeComponent()

        shapeLayer = New MapShapeLayer()
        MyMap.ShapeLayers.Add(shapeLayer)EndSubPrivateSub ClearMapBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
        ClearMap()EndSubPrivateSub ScalingPinBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)EndSubPrivateSub DropPinBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)EndSubPrivateSub BouncePinBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)EndSubPrivate Async Sub Bounce4PinsBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)EndSubPrivateSub MovePinOnPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)EndSubPrivateSub MovePinOnGeodesicPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)EndSubPrivateSub MoveMapOnPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)EndSubPrivateSub MoveMapOnGeodesicPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)EndSubPrivateSub DrawPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)EndSubPrivateSub DrawGeodesicPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)EndSubPrivateSub ClearMap()
        MyMap.Children.Clear()
        shapeLayer.Shapes.Clear()EndSubEndClass

 

If you run the application you will see the map and a bunch of buttons appearing in a panel like this:

Screenshot: Buttons appearing in a panel

Screenshot: Buttons appearing in a panel

Creating animations using XAML

You can create fairly complex animations in XAML using styles, storyboard’s, double animation’s, and render transforms. These are great for basic animations that animate a property’s value from one value to another linearly. For more complex animations there is also Key-frame animation classes.

To try this out lets create a simple animation that scales the size of a pushpin to twice its size when the mouse is hovered over it. To do this we first need to create a simple storyboard that uses a render transform and double animations to scale in the X and Y directions. To do this open the App.xaml file and update it with the following XAML:

<Applicationx:Class="AnimatedMaps.App"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="using:AnimatedMaps"><Application.Resources><Storyboardx:Key="expandStoryboard"><DoubleAnimationTo="2"Duration="0:0:0.2"Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)"/><DoubleAnimationTo="2"Duration="0:0:0.2"Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)"/>Storyboard>Application.Resources>Application>

 

To apply the storyboard to the pushpin we will use the PointerEntered event to add the storyboard to the pushpin and start the animation. We will also use the PointerExited event to stop the animation. The storyboard animates the X and Y values of a scale transform, as such we need to set the RenderTransform property of the pushpin to a ScaleTransform. We will also set the RenderTransformOrigin such that it scales out from the center of the pushpin. To do all this open the MainPage.xaml.cs file and update the ScalingPinBtn_Tapped event handler with the following code:

privatevoid ScalingPinBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    ClearMap();

    var pin = new Pushpin();
    MapLayer.SetPosition(pin, MyMap.Center);

    pin.RenderTransformOrigin = new Windows.Foundation.Point(0.5, 0.5);
    pin.RenderTransform = new ScaleTransform();

    var story = (Storyboard)App.Current.Resources["expandStoryboard"];

    pin.PointerEntered += (s, a) =>
    {
        story.Stop();
        Storyboard.SetTarget(story, pin);
        story.Begin();
    };

    pin.PointerExited += (s, a) =>
    {
        story.Stop();
    };

    MyMap.Children.Add(pin);
}

 

If using Visual Basic update the MainPage.xaml.vb file with the following code:

PrivateSub ScalingPinBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
    ClearMap()Dim pin = New Pushpin()
    MapLayer.SetPosition(pin, MyMap.Center)

    pin.RenderTransformOrigin = New Windows.Foundation.Point(0.5, 0.5)
    pin.RenderTransform = New ScaleTransform()Dim story = DirectCast(App.Current.Resources("expandStoryboard"), Storyboard)AddHandler pin.PointerEntered, Sub(s, a)
                                       story.Stop()
                                       Storyboard.SetTarget(story, pin)
                                       story.Begin()EndSubAddHandler pin.PointerExited, Sub(s, a)
                                      story.Stop()EndSub

    MyMap.Children.Add(pin)
EndSub

 

If you run the app and press the “Scale on Hover” button a pushpin will appear in the center of the map. If you then hover your mouse over the pushpin you will notice that it grows to be twice its size. When you hover off the pushpin it goes back to its original size. Here is an animated gif that demonstrates this animation:

Animation: Pushpin scale on hover

Example: Pin scale

Creating simple pushpin animations

So far we have seen a simple way to animate pushpins using XAML. One caveat of the XAML animation is that it can only run against a single element at a time. If we tried to run a XAML animation on a second pushpin without stopping the animation for the first one, an error would occur. To get around this we can dynamically create a storyboard using code. In this section we will create a class that provides us with some tools to animate pushpins in the Y-axis. There are two main animation effects we will create, one to drop the pushpins from a height above the map and another to drop it from a height and have it bounce to rest on the map. If we were to create a storyboard for these animations in XAML they would look something like this:

<Storyboardx:Name="dropStoryboard"><DoubleAnimationFrom="-150"To="0"Duration="00:00:0.4"Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"><DoubleAnimation.EasingFunction><QuadraticEaseEasingMode="EaseIn"/>DoubleAnimation.EasingFunction>DoubleAnimation>Storyboard><Storyboardx:Name="bounceStoryboard"><DoubleAnimationFrom="-150"To="0"Duration="00:00:1"Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"><DoubleAnimation.EasingFunction><BounceEaseBounces="2"EasingMode="EaseOut"Bounciness="2"/>DoubleAnimation.EasingFunction>DoubleAnimation>Storyboard>

 

To do dynamically create these storyboards in code, create a new folder called Animations. In this folder create a new class called PushpinAnimations. In this class we will create three methods. The first will be a generic method that creates a storyboard that animates a pushpin along the Y-axis. The second method will make use of the first method and pass in a QuadraticEase function so as to create the drop effect. The third method will also make use of the second method and pass in a BounceEase function to create the bounce effect. Putting this all together, update PushpinAnimations.cs file with the following code:

using Bing.Maps;using System;using Windows.UI.Xaml;using Windows.UI.Xaml.Media;using Windows.UI.Xaml.Media.Animation;namespace AnimatedMaps.Animations
{publicstaticclass PushpinAnimations
    {publicstaticvoid AnimateY(UIElement pin, double fromY, double toY, int duration, EasingFunctionBase easingFunction)
        {
            pin.RenderTransform = new TranslateTransform();

            var sb = new Storyboard();
            var animation = new DoubleAnimation()
            {
                From = fromY,
                To = toY,
                Duration = new TimeSpan(0, 0, 0, 0, duration),
                EasingFunction = easingFunction
            };

            Storyboard.SetTargetProperty(animation, "(UIElement.RenderTransform).(TranslateTransform.Y)");
            Storyboard.SetTarget(animation, pin);

            sb.Children.Add(animation);
            sb.Begin();
        }

        publicstaticvoid Drop(UIElement pin, double? height, int? duration)
        {           
            height = (height.HasValue && height.Value > 0) ? height : 150;
            duration = (duration.HasValue && duration.Value > 0) ? duration : 150;

            var anchor = MapLayer.GetPositionAnchor(pin);
            var from = anchor.Y + height.Value;

            AnimateY(pin, -from, -anchor.Y, duration.Value, new QuadraticEase()
            {
                EasingMode = EasingMode.EaseIn
            });
        }publicstaticvoid Bounce(UIElement pin, double? height, int? duration)
        {
            height = (height.HasValue && height.Value > 0) ? height : 150;
            duration = (duration.HasValue && duration.Value > 0) ? duration : 1000;

            var anchor = MapLayer.GetPositionAnchor(pin);
            var from = anchor.Y + height.Value;

            AnimateY(pin, -from, -anchor.Y, duration.Value, new BounceEase()
                {
                    Bounces = 2,
                    EasingMode = EasingMode.EaseOut,
                    Bounciness = 2
                });
        }
    }
}

 

If using Visual Basic update the PushpinAnimations.vb file with the following code:

Imports Bing.MapsImports Windows.FoundationImports Windows.UI.XamlImports Windows.UI.Xaml.Media.AnimationNamespace AnimatedMaps.AnimationsPublicNotInheritableClass PushpinAnimationsPublicSharedSub AnimateY(pin As UIElement, fromY AsDouble, toY AsDouble, duration AsInteger, easingFunction As EasingFunctionBase)
            pin.RenderTransform = New TranslateTransform()Dim sb = New Storyboard()Dim Animation = New DoubleAnimation()
            Animation.From = fromY
            Animation.To = toY
            Animation.Duration = New System.TimeSpan(0, 0, 0, 0, duration)
            Animation.EasingFunction = easingFunction

            Storyboard.SetTargetProperty(Animation, "(UIElement.RenderTransform).(TranslateTransform.Y)")
            Storyboard.SetTarget(Animation, pin)

            sb.Children.Add(Animation)
            sb.Begin()
        EndSubPublicSharedSub Drop(pin As UIElement, height As System.Nullable(Of Double), duration As System.Nullable(Of Integer))
            height = If((height.HasValue AndAlso height.Value > 0), height, 150)
            duration = If((duration.HasValue AndAlso duration.Value > 0), duration, 150)Dim anchor = MapLayer.GetPositionAnchor(pin)Dim from = anchor.Y + height.ValueDim easing = New QuadraticEase()
            easing.EasingMode = EasingMode.EaseIn

            AnimateY(pin, -from, -anchor.Y, duration, easing)
        EndSubPublicSharedSub Bounce(pin As UIElement, height As System.Nullable(Of Double), duration As System.Nullable(Of Integer))
            height = If((height.HasValue AndAlso height.Value > 0), height, 150)
            duration = If((duration.HasValue AndAlso duration.Value > 0), duration, 1000)Dim anchor = MapLayer.GetPositionAnchor(pin)Dim from = anchor.Y + height.ValueDim easing = New BounceEase()
            easing.Bounces = 2
            easing.EasingMode = EasingMode.EaseOut
            easing.Bounciness = 2

            AnimateY(pin, -from, -anchor.Y, duration, easing)
        EndSubEndClassEndNamespace

 

To implement the drop animation update the DropPinBtn_Tapped event handler in the MainPage.xaml.cs file with the following code:

privatevoid DropPinBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    ClearMap();
    var pin = new Pushpin();
    MapLayer.SetPosition(pin, MyMap.Center);

    MyMap.Children.Add(pin);

    Animations.PushpinAnimations.Drop(pin, null, null);
}

 

If using Visual Basic update the MainPage.xaml.vb file with the following code:

PrivateSub DropPinBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
    ClearMap()Dim pin = New Pushpin()
    MapLayer.SetPosition(pin, MyMap.Center)

    MyMap.Children.Add(pin)

    AnimatedMaps.Animations.PushpinAnimations.Drop(pin, Nothing, Nothing)EndSub

 

If you run the application and press the “Drop Pin” button it will drop a pushpin from 150 pixels above the map to the center of the map like this:

Example: Drop pushpin

Example: Drop pin

To implement the drop animation update the BouncePinBtn_Tapped event handler in the MainPage.xaml.cs file with the following code:

privatevoid BouncePinBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    ClearMap();

    var pin = new Pushpin();
    MapLayer.SetPosition(pin, MyMap.Center);

    MyMap.Children.Add(pin);

    Animations.PushpinAnimations.Bounce(pin, null, null);
}

 

If using Visual Basic update the MainPage.xaml.vb file with the following code:

PrivateSub BouncePinBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
    ClearMap()Dim pin = New Pushpin()
    MapLayer.SetPosition(pin, MyMap.Center)

    MyMap.Children.Add(pin)

    AnimatedMaps.Animations.PushpinAnimations.Bounce(pin, Nothing, Nothing)EndSub

 

If you run the application and press the “Bounce Pin” button it will drop a pushpin from 150 pixels above the map add bounce to rest ato the center of the map like this:

Example: Bounce pushpin

Example: Bounce pin

To implement the drop animation update the Bounce4PinsBtn_Tapped event handler in the MainPage.xaml.cs file with the following code:

private async void Bounce4PinsBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    ClearMap();for (var i = 0; i < path.Count; i++) {
        var pin = new Pushpin();
        MapLayer.SetPosition(pin, path[i]);

        MyMap.Children.Add(pin);

        Animations.PushpinAnimations.Bounce(pin, null, null);

        await Task.Delay(500);
    }       
}

 

If using Visual Basic update the MainPage.xaml.vb file with the following code:

Private Async Sub Bounce4PinsBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
    ClearMap()For i AsInteger = 0 To path.Count - 1Dim pin = New Pushpin()
        MapLayer.SetPosition(pin, path(i))

        MyMap.Children.Add(pin)

        AnimatedMaps.Animations.PushpinAnimations.Bounce(pin, Nothing, Nothing)

        Await Task.Delay(500)
    NextEndSub

 

If you run the application and press the “Bounce 4 Pins” button it will drop 4 pushpin from 150 pixels above the map with a delay of 500ms between each add bounce them to rest at the center of the map like this:

Example: Bounce four pushpins

Example: Bounce four pins

Creating path animations

The animations we have seen so far have been fairly simple and only run once. We can create more complex custom animations by using a DispatcherTimer. One common type of animation I see developers struggle with when working with maps is animating along a path. To get a sense of the complexity involved consider the path between two locations on the map. If asked you to draw the shortest path between these two locations your first instinct might be to draw straight line, and visually you would be correct. However, the world is not flat and is actually an ellipsoid, yet most online maps show the world as a flat 2D rectangle. In order to accomplish this the map projects the 3D ellipsoid to this 2D map using what is called a Mercator projection. This ends up stretching the map out at the poles. So what does this all mean, well it means that the shortest distance between two locations on the map is rarely a straight line and is actually a curved path, commonly referred to as a geodesic path. Here is an image with a path connecting Seattle, New York, London and Italy. The red line connects these locations using straight lines while the purple line shows the equivalent geodesic path.

Example: Geodesic and straight paths

Example: Geodesic and straight paths

So which type of line do you want to animate with? Straight line paths are great for generic animations where you want to move things across the screen and only really care about the start and end point. Whereas geodesic lines are great for when you want the path to be spatially accurate, such as when animating the path of an airplane. It’s worth noting that when you are working with short distances the differences between are very minor.

To get started we will create a path animation class that will takes in a LocationCollection of points for the path, a callback function that will get called on each frame of the animation, a boolean to indicate if the path should follow a geodesic path or not, and a duration time in milliseconds for how long the animation should take from start to finish. This class will also wrap a DispatcherTimer with methods to support play, pause and stop functions. This class will have a private method called PreCalculate which we will use later to calculate the animation frames when the PathAnimation is created. When the path animation is created it will pre-calculate all the midpoint locations that the animation passes through. As a result little to no calculations will be needed when the animation advances from frame to frame and will result in a nice smooth animation. To create a new class file called PathAnimation.cs in the Animation folder of the project and update the code in this file with the following:

using Bing.Maps;using System;using System.Collections.Generic;using Windows.UI.Xaml;namespace AnimatedMaps.Animations
{publicclass PathAnimation
    {privateconstint _delay = 30;privateconstdouble EARTH_RADIUS_KM = 6378.1;private DispatcherTimer _timerId;private LocationCollection _path;privatebool _isGeodesic = false;private LocationCollection _intervalLocs;private List<int> _intervalIdx;privateint? _duration;privateint _frameIdx = 0;privatebool _isPaused;public PathAnimation(LocationCollection path, IntervalCallback intervalCallback, bool isGeodesic, int? duration)
        {
            _path = path;
            _isGeodesic = isGeodesic;
            _duration = duration;

            PreCalculate();

            _timerId = new DispatcherTimer();
            _timerId.Interval = new TimeSpan(0, 0, 0, 0, _delay);

            _timerId.Tick += (s, a) =>
            {if (!_isPaused)
                {double progress = (double)(_frameIdx * _delay) / (double)_duration.Value;if (progress > 1)
                    {
                        progress = 1;
                    }if (intervalCallback != null)
                    {
                        intervalCallback(_intervalLocs[_frameIdx], _intervalIdx[_frameIdx], _frameIdx);
                    }if (progress == 1)
                    {
                        _timerId.Stop();
                    }

                    _frameIdx++;
                }
            };
        }

        publicdelegatevoid IntervalCallback(Location loc, int pathIdx, int frameIdx);public LocationCollection Path
        {
            get { return _path; }
            set
            {
                _path = value;
                PreCalculate();
            }
        }publicbool IsGeodesic
        {
            get { return _isGeodesic; }
            set 
            { 
                _isGeodesic = value;
                PreCalculate();
            }
        }publicint? Duration
        {
            get { return _duration; }
            set
            {
                _duration = value;
                PreCalculate();
            }
        }publicvoid Play()
        {
            _frameIdx = 0;
            _isPaused = false;
            _timerId.Start();
        }publicvoid Pause()
        {
            _isPaused = true;
        }publicvoid Stop()
        {if (_timerId.IsEnabled)
            {
                _frameIdx = 0;
                _isPaused = false;
                _timerId.Stop();
            }
        }privatevoid PreCalculate()
        {
        }
    }
}

 

If using Visual Basic update the PathAnimation.vb file with the following code:

Imports Bing.MapsImports System.Collections.GenericImports Windows.UI.XamlNamespace AnimatedMaps.AnimationsPublicClass PathAnimationPrivateConst _delay AsInteger = 30PrivateConst EARTH_RADIUS_KM AsDouble = 6378.1Private _timerId As DispatcherTimerPrivate _path As LocationCollectionPrivate _isGeodesic AsBoolean = FalsePrivate _intervalLocs As LocationCollectionPrivate _intervalIdx As List(Of Integer)Private _duration As System.Nullable(Of Integer)Private _frameIdx AsInteger = 0Private _isPaused AsBooleanPublicSubNew(path As LocationCollection, intervalCallback As IntervalCallback, isGeodesic AsBoolean, duration As System.Nullable(Of Integer))
            _path = path
            _isGeodesic = isGeodesic
            _duration = duration

            PreCalculate()

            _timerId = New DispatcherTimer()
            _timerId.Interval = New TimeSpan(0, 0, 0, 0, _delay)AddHandler _timerId.Tick, Sub(s, a)IfNot _isPaused ThenDim progress AsDouble = CDbl(_frameIdx * _delay) / CDbl(_duration.Value)If progress > 1 Then
                                                  progress = 1EndIf

                                              intervalCallback(_intervalLocs(_frameIdx), _intervalIdx(_frameIdx), _frameIdx)

                                              If progress = 1 Then
                                                  _timerId.[Stop]()EndIf

                                              _frameIdx += 1
                                          EndIfEndSubEndSubPublicDelegateSub IntervalCallback(loc As Location, pathIdx AsInteger, frameIdx AsInteger)PublicProperty Path() As LocationCollectionGetReturn _pathEndGetSet(value As LocationCollection)
                _path = value
                PreCalculate()EndSetEndPropertyPublicProperty IsGeodesic() AsBooleanGetReturn _isGeodesicEndGetSet(value AsBoolean)
                _isGeodesic = value
                PreCalculate()EndSetEndPropertyPublicProperty Duration() As System.Nullable(Of Integer)GetReturn _durationEndGetSet(value As System.Nullable(Of Integer))
                _duration = value
                PreCalculate()EndSetEndPropertyPublicSub Play()
            _frameIdx = 0
            _isPaused = False
            _timerId.Start()EndSubPublicSub Pause()
            _isPaused = TrueEndSubPublicSub [Stop]()If _timerId.IsEnabled Then
                _frameIdx = 0
                _isPaused = False
                _timerId.[Stop]()EndIfEndSubPrivateSub PreCalculate()EndSubEndClassEndNamespace

 

To help us with the geodesic path calculations we will add some private helper methods to calculate the Haversine distance between two locations (distance along curvature of the earth), bearing and a destination coordinate. Add the following methods to the PathAnimation class.

privatestaticdouble DegToRad(double x)
{return x * Math.PI / 180;
}privatestaticdouble RadToDeg(double x)
{return x * 180 / Math.PI;
}privatestaticdouble HaversineDistance(Location origin, Location dest)
{double lat1 = DegToRad(origin.Latitude),
        lon1 = DegToRad(origin.Longitude),
        lat2 = DegToRad(dest.Latitude),
        lon2 = DegToRad(dest.Longitude);double dLat = lat2 - lat1,
    dLon = lon2 - lon1,
    cordLength = Math.Pow(Math.Sin(dLat / 2), 2) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Pow(Math.Sin(dLon / 2), 2),
    centralAngle = 2 * Math.Atan2(Math.Sqrt(cordLength), Math.Sqrt(1 - cordLength));return EARTH_RADIUS_KM * centralAngle;
}privatestaticdouble CalculateBearing(Location origin, Location dest)
{
    var lat1 = DegToRad(origin.Latitude);
    var lon1 = origin.Longitude;
    var lat2 = DegToRad(dest.Latitude);
    var lon2 = dest.Longitude;
    var dLon = DegToRad(lon2 - lon1);
    var y = Math.Sin(dLon) * Math.Cos(lat2);
    var x = Math.Cos(lat1) * Math.Sin(lat2) - Math.Sin(lat1) * Math.Cos(lat2) * Math.Cos(dLon);return (RadToDeg(Math.Atan2(y, x)) + 360) % 360;
}privatestatic Location CalculateCoord(Location origin, double brng, double arcLength)
{double lat1 = DegToRad(origin.Latitude),
        lon1 = DegToRad(origin.Longitude),
        centralAngle = arcLength / EARTH_RADIUS_KM;

    var lat2 = Math.Asin(Math.Sin(lat1) * Math.Cos(centralAngle) + Math.Cos(lat1) * Math.Sin(centralAngle) * Math.Cos(DegToRad(brng)));
    var lon2 = lon1 + Math.Atan2(Math.Sin(DegToRad(brng)) * Math.Sin(centralAngle) * Math.Cos(lat1), Math.Cos(centralAngle) - Math.Sin(lat1) * Math.Sin(lat2));

 returnnew Location(RadToDeg(lat2), RadToDeg(lon2));
}

 

If using Visual Basic update the PathAnimation.vb file with the following code:

PrivateSharedFunction DegToRad(x AsDouble) AsDoubleReturn x * Math.PI / 180EndFunctionPrivateSharedFunction RadToDeg(x AsDouble) AsDoubleReturn x * 180 / Math.PIEndFunctionPrivateSharedFunction HaversineDistance(origin As Location, dest As Location) AsDoubleDim lat1 AsDouble = DegToRad(origin.Latitude), lon1 AsDouble = DegToRad(origin.Longitude), lat2 AsDouble = DegToRad(dest.Latitude), lon2 AsDouble = DegToRad(dest.Longitude)Dim dLat AsDouble = lat2 - lat1, dLon AsDouble = lon2 - lon1, cordLength AsDouble = Math.Pow(Math.Sin(dLat / 2), 2) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Pow(Math.Sin(dLon / 2), 2), centralAngle AsDouble = 2 * Math.Atan2(Math.Sqrt(cordLength), Math.Sqrt(1 - cordLength))Return EARTH_RADIUS_KM * centralAngleEndFunctionPrivateSharedFunction CalculateBearing(origin As Location, dest As Location) AsDoubleDim lat1 = DegToRad(origin.Latitude)Dim lon1 = origin.LongitudeDim lat2 = DegToRad(dest.Latitude)Dim lon2 = dest.LongitudeDim dLon = DegToRad(lon2 - lon1)Dim y = Math.Sin(dLon) * Math.Cos(lat2)Dim x = Math.Cos(lat1) * Math.Sin(lat2) - Math.Sin(lat1) * Math.Cos(lat2) * Math.Cos(dLon)Return (RadToDeg(Math.Atan2(y, x)) + 360) Mod 360EndFunctionPrivateSharedFunction CalculateCoord(origin As Location, brng AsDouble, arcLength AsDouble) As LocationDim lat1 AsDouble = DegToRad(origin.Latitude), lon1 AsDouble = DegToRad(origin.Longitude), centralAngle AsDouble = arcLength / EARTH_RADIUS_KMDim lat2 = Math.Asin(Math.Sin(lat1) * Math.Cos(centralAngle) + Math.Cos(lat1) * Math.Sin(centralAngle) * Math.Cos(DegToRad(brng)))Dim lon2 = lon1 + Math.Atan2(Math.Sin(DegToRad(brng)) * Math.Sin(centralAngle) * Math.Cos(lat1), Math.Cos(centralAngle) - Math.Sin(lat1) * Math.Sin(lat2))ReturnNew Location(RadToDeg(lat2), RadToDeg(lon2))EndFunction

 

At this point the only thing left to do with the PathAnimation class is to create the code for the PreCalculate method. When this method is called it will calculate all the midpoint locations on the path for every frame in the animation. As a result little to no calculations needing to be performed when the animation advances a frame and thus should create a smooth animation.

Animating along a straight path is fairly easy. One method is to calculate the latitude and longitude differences between two locations and then divide these values by the number of frames in the animation to get a single frame offset values for latitude and longitude. Then when each frame is animated we take the last calculate coordinate and add these offsets to the latitude and longitude properties to get the new coordinate to advance the animation to.

Animating along a geodesic path is a bit more difficult. One of our Bing Maps MVP’s, Alastair Aitchison, wrote a great blog post on creating geodesic lines in Bing Maps. The process of creating a geodesic line consists of calculating a several midpoint locations that are between two points. This can be done by calculating the distance and bearing between the two locations. Once you have this you can divide the distance by the number of mid-points you want to have and then use the distance to each midpoint and the bearing between the two end points to calculate the coordinate of the mid-point location.

To do all this update the PreCalculate method in the PathAnimation class with the following code:

privatevoid PreCalculate()
{//Stop the timerif (_timerId != null&& _timerId.IsEnabled)
    {
        _timerId.Stop();
    }

    _duration = (_duration.HasValue && _duration.Value > 0) ? _duration : 150; 

    _intervalLocs = new LocationCollection();
    _intervalIdx = new List<int>();

    _intervalLocs.Add(_path[0]);
    _intervalIdx.Add(0);
                        
    double dlat, dlon;double totalDistance = 0;if (_isGeodesic)
    {//Calcualte the total distance along the path in KM's.for (var i = 0; i < _path.Count - 1; i++)
        {
            totalDistance += HaversineDistance(_path[i], _path[i + 1]);
        }
    }else
    {//Calcualte the total distance along the path in degrees.for (var i = 0; i < _path.Count - 1; i++)
        {
            dlat = _path[i + 1].Latitude - _path[i].Latitude;
            dlon = _path[i + 1].Longitude - _path[i].Longitude;

            totalDistance += Math.Sqrt(dlat * dlat + dlon * dlon);
        }
    }

    int frameCount = (int)Math.Ceiling((double)_duration.Value / (double)_delay);int idx = 0;double progress;//Pre-calculate step points for smoother rendering.for (var f = 0; f < frameCount; f++)
    {
        progress = (double)(f * _delay) / (double)_duration.Value;double travel = progress * totalDistance;double alpha = 0;double dist = 0;double dx = travel;for (var i = 0; i < _path.Count - 1; i++)
        {if (_isGeodesic)
            {
                dist += HaversineDistance(_path[i], _path[i + 1]);
            }else
            {
                dlat = _path[i + 1].Latitude - _path[i].Latitude;
                dlon = _path[i + 1].Longitude - _path[i].Longitude;
                alpha = Math.Atan2(dlat * Math.PI / 180, dlon * Math.PI / 180);
                dist += Math.Sqrt(dlat * dlat + dlon * dlon);
            }if (dist >= travel)
            {
                idx = i;break;
            }

            dx = travel - dist;
        }

        if (dx != 0 && idx < _path.Count - 1)
        {if (_isGeodesic)
            {
                var bearing = CalculateBearing(_path[idx], _path[idx + 1]);
                _intervalLocs.Add(CalculateCoord(_path[idx], bearing, dx));
            }else
            {
                dlat = dx * Math.Sin(alpha);
                dlon = dx * Math.Cos(alpha);

                _intervalLocs.Add(new Location(_path[idx].Latitude + dlat, _path[idx].Longitude + dlon));
            }

            _intervalIdx.Add(idx);
        }
    }

    //Ensure the last location is the last coordinate in the path.
    _intervalLocs.Add(_path[_path.Count - 1]);
    _intervalIdx.Add(_path.Count - 1);
}

 

If using Visual Basic update the PathAnimation.vb file with the following code:

PrivateSub PreCalculate()'Stop the timerIf _timerId IsNot NothingAndAlso _timerId.IsEnabled Then
        _timerId.[Stop]()EndIf

    _duration = If((_duration.HasValue AndAlso _duration.Value > 0), _duration, 150)

    _intervalLocs = New LocationCollection()
    _intervalIdx = New List(Of Integer)()

    _intervalLocs.Add(_path(0))
    _intervalIdx.Add(0)

    Dim dlat AsDouble, dlon AsDoubleDim totalDistance AsDouble = 0If _isGeodesic Then'Calcualte the total distance along the path in KM's.For i AsInteger = 0 To _path.Count - 2
            totalDistance += HaversineDistance(_path(i), _path(i + 1))NextElse'Calcualte the total distance along the path in degrees.For i AsInteger = 0 To _path.Count - 2
            dlat = _path(i + 1).Latitude - _path(i).Latitude
            dlon = _path(i + 1).Longitude - _path(i).Longitude

            totalDistance += Math.Sqrt(dlat * dlat + dlon * dlon)
        NextEndIfDim frameCount AsInteger = CInt(Math.Ceiling(CDbl(_duration.Value) / CDbl(_delay)))Dim idx AsInteger = 0Dim progress AsDouble'Pre-calculate step points for smoother rendering.For f AsInteger = 0 To frameCount - 1
        progress = CDbl(f * _delay) / CDbl(_duration.Value)Dim travel AsDouble = progress * totalDistanceDim alpha AsDouble = 0Dim dist AsDouble = 0Dim dx AsDouble = travelFor i AsInteger = 0 To _path.Count - 2If _isGeodesic Then
                dist += HaversineDistance(_path(i), _path(i + 1))Else
                dlat = _path(i + 1).Latitude - _path(i).Latitude
                dlon = _path(i + 1).Longitude - _path(i).Longitude
                alpha = Math.Atan2(dlat * Math.PI / 180, dlon * Math.PI / 180)
                dist += Math.Sqrt(dlat * dlat + dlon * dlon)EndIfIf dist >= travel Then
                idx = iExitForEndIf

            dx = travel - dist
        NextIf dx <> 0 AndAlso idx < _path.Count - 1 ThenIf _isGeodesic ThenDim bearing = CalculateBearing(_path(idx), _path(idx + 1))
                _intervalLocs.Add(CalculateCoord(_path(idx), bearing, dx))Else
                dlat = dx * Math.Sin(alpha)
                dlon = dx * Math.Cos(alpha)

                _intervalLocs.Add(New Location(_path(idx).Latitude + dlat, _path(idx).Longitude + dlon))EndIf

            _intervalIdx.Add(idx)
        EndIfNext'Ensure the last location is the last coordinate in the path.
    _intervalLocs.Add(_path(_path.Count - 1))
    _intervalIdx.Add(_path.Count - 1)EndSub

 

Implementing the path animations

Now that the path animation class is created we can start implementing it. Before we start adding animations we will add a property to the MainPage class that keeps track of the current path animation. In addition to this we will also update the ClearMap method so that it will stop any currently running path animaitons. Use the following code to add the current animation property to the MainPage class and update the ClearMap method.

private Animations.PathAnimation currentAnimation;privatevoid ClearMap()
{
    MyMap.Children.Clear();
    shapeLayer.Shapes.Clear();if (currentAnimation != null)
    {
        currentAnimation.Stop();
        currentAnimation = null;
    }
}

 

If using Visual Basic update the MainPage.xaml.vb file with the following code:

Private currentAnimation As AnimatedMaps.Animations.PathAnimationPrivateSub ClearMap()
    MyMap.Children.Clear()
    shapeLayer.Shapes.Clear()If currentAnimation IsNot NothingThen
        currentAnimation.Stop()
        currentAnimation = NothingEndIfEndSub

 

The first path animation we will implement will move a pushpin along either a straight or geodesic path. In the MainPage.xaml.cs file, use the following code to update the MovePinOnPathBtn_Tapped and MovePinOnGeodesicPathBtn_Tapped button handlers and add a new method called MovePinOnPath.

privatevoid MovePinOnPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    MovePinOnPath(false);
}privatevoid MovePinOnGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    MovePinOnPath(true);
}privatevoid MovePinOnPath(bool isGeodesic)
{
    ClearMap();
    var pin = new Pushpin();
    MapLayer.SetPosition(pin, path[0]);
    MyMap.Children.Add(pin);

    currentAnimation = new Animations.PathAnimation(path, (coord, pathIdx, frameIdx) =>
    {
        MapLayer.SetPosition(pin, coord);
    }, isGeodesic, 10000);

    currentAnimation.Play();
}

 

If using Visual Basic update the MainPage.xaml.vb file with the following code:

PrivateSub MovePinOnPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
    MovePinOnPath(False)EndSubPrivateSub MovePinOnGeodesicPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
    MovePinOnPath(True)EndSubPrivateSub MovePinOnPath(isGeodesic AsBoolean)
    ClearMap()Dim pin = New Pushpin()
    MapLayer.SetPosition(pin, path(0))

    MyMap.Children.Add(pin)

    currentAnimation = New AnimatedMaps.Animations.PathAnimation(path,Sub(coord, pathIdx, frameIdx)
                MapLayer.SetPosition(pin, coord)EndSub, isGeodesic, 10000)

    currentAnimation.Play()
EndSub

 

If you run the application and press the “Move Pin Along Path” or “Move Pin Along Geodesic Path” button you will see a pushpin follow a straight or geodesic line between the path locations. The following animated gifs show what these animations look like.

Example: Draw straight line path

Example: Draw straight line path

 

Example: Draw geodesic path

Example: Draw geodesic path

 

The next path animation we will implement will move the map along either a straight or geodesic path. Update the MoveMapOnPathBtn_Tapped and MoveMapOnGeodesicPathBtn_Tapped button handlers and add a new method called MoveMapOnPath.

privatevoid MoveMapOnPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    MoveMapOnPath(false);
}privatevoid MoveMapOnGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    MoveMapOnPath(true);
}privatevoid MoveMapOnPath(bool isGeodesic)
{
    ClearMap();//Change zooms levels as map reaches points along path.int[] zooms = newint[4] { 5, 4, 6, 5 };

    MyMap.SetView(path[0], zooms[0]);

    currentAnimation = new Animations.PathAnimation(path, (coord, pathIdx, frameIdx) =>
    {
        MyMap.SetView(coord, zooms[pathIdx]);
    }, isGeodesic, 10000);

    currentAnimation.Play();
}

 

If using Visual Basic update the MainPage.xaml.vb file with the following code:

PrivateSub MoveMapOnPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
    MoveMapOnPath(False)EndSubPrivateSub MoveMapOnGeodesicPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
    MoveMapOnPath(True)EndSubPrivateSub MoveMapOnPath(isGeodesic AsBoolean)
    ClearMap()'Change zooms levels as map reaches points along path.Dim zooms AsInteger() = NewInteger(3) {5, 4, 6, 5}

    MyMap.SetView(path(0), zooms(0))

    currentAnimation = New AnimatedMaps.Animations.PathAnimation(path,Sub(coord, pathIdx, frameIdx)
            MyMap.SetView(coord, zooms(pathIdx))EndSub, isGeodesic, 10000)

    currentAnimation.Play()
EndSub

 

Pressing the “Move Map Along Path” or “Move Map Along Geodesic Path” buttons you will see the map pan from one location to another, while changing zoom levels when it passes one of the path points. I have not included animated gif’s for this animation as they ended up being several megabytes in size.

The final path animation we will implement will animate the drawing of the path line. Update the DrawPathBtn_Tapped and DrawGeodesicPathBtn_Tapped button handlers and add a new method called DrawPath.

privatevoid DrawPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    DrawPath(false);
}privatevoid DrawGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    DrawPath(true);
}privatevoid DrawPath(bool isGeodesic)
{
    ClearMap();

    MapPolyline line = new MapPolyline()
    {
        Color = Colors.Red,
        Width = 4
    };

    currentAnimation = new Animations.PathAnimation(path, (coord, pathIdx, frameIdx) =>
    {if (frameIdx == 1)
        {//Create the line after the first frame so that we have two points to work with.                    
            line.Locations = new LocationCollection() { path[0], coord };
            shapeLayer.Shapes.Add(line);
        }elseif (frameIdx > 1)
        {
            line.Locations.Add(coord);
        }
    }, isGeodesic, 10000);

    currentAnimation.Play();
}

 

If using Visual Basic update the MainPage.xaml.vb file with the following code:

PrivateSub DrawPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
    DrawPath(False)EndSubPrivateSub DrawGeodesicPathBtn_Tapped(sender AsObject, e As TappedRoutedEventArgs)
    DrawPath(True)EndSubPrivateSub DrawPath(isGeodesic AsBoolean)
    ClearMap()Dim line AsNew MapPolyline()
    line.Color = Colors.Red
    line.Width = 4

    currentAnimation = New AnimatedMaps.Animations.PathAnimation(path,Sub(coord, pathIdx, frameIdx)If frameIdx = 1 Then'Create the line the line after the first frame so that we have two points to work with.                    
                        line.Locations = New LocationCollection() From { _
                        path(0), _
                        coord _
                    }
                        shapeLayer.Shapes.Add(line)ElseIf frameIdx > 1 Then
                        line.Locations.Add(coord)EndIfEndSub, isGeodesic, 10000)

    currentAnimation.Play()
EndSub

 

If you run the application and press the “Draw Path” or “Draw Geodesic Path” button you will see a pushpin follow a straight or geodesic line between the path locations. The following animated gifs show what these animations look like.

Example: Draw straight line path with pushpin

Example: Draw straight line path with pin

 

Example: Draw geodesic path with pushpin

Example: Draw geodesic path with pin

Wrapping Up

In this blog we have seen a number of different ways to animate data on Bing Maps. Let your imagination go wild and create some cool animations. As mentioned at the beginning of this blog post the full source code can be found in the MSDN Code Samples here. Also, these animations work great with both the Bing Maps Windows Store and WPF controls. The pushpin animations require a small amount of changes to get them to work correctly in WPF, however you can easily find these here.

- Ricky Brundritt, Bing Maps Program Manager


Viewing all articles
Browse latest Browse all 10804

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>