Udostępnij za pośrednictwem


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

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:

 <Page
    x:Class="AnimatedMaps.MainPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AnimatedMaps"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:m="using:Bing.Maps"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <m:Map Name="MyMap" Credentials="YOUR_BING_MAPS_KEY"/>

        <Grid Width="270" Height="610" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="10">
            <Border Background="Black" Opacity="0.8" CornerRadius="10"/>
            
            <StackPanel Margin="10">
                <Button Content="Clear Map" Tapped="ClearMapBtn_Tapped" Height="45" Width="120"/>

                <TextBlock Text="Pushpin Animations" FontSize="16" FontWeight="Bold" Margin="0,10,0,0"/>
                <Button Content="Scaling Pin" Tapped="ScalingPinBtn_Tapped" Height="45" Width="120"/>
                
                <TextBlock Text="Pushpin Animations" FontSize="16" FontWeight="Bold" Margin="0,10,0,0"/>
                <Button Content="Drop Pin" Tapped="DropPinBtn_Tapped" Height="45" Width="120"/>
                <Button Content="Bounce Pin" Tapped="BouncePinBtn_Tapped" Height="45" Width="120"/>
                <Button Content="Bounce 4 Pins After Each Other" Tapped="Bounce4PinsBtn_Tapped" Height="45" Width="250"/>
            
                <TextBlock Text="Path Animations" FontSize="16" FontWeight="Bold" Margin="0,10,0,0"/>
                <Button Content="Move Pin Along Path" Tapped="MovePinOnPathBtn_Tapped" Height="45" Width="250"/>
                <Button Content="Move Pin Along Geodesic Path" Tapped="MovePinOnGeodesicPathBtn_Tapped" Height="45" Width="250"/>
                <Button Content="Move Map Along Path" Tapped="MoveMapOnPathBtn_Tapped" Height="45" Width="250"/>
                <Button Content="Move Map Along Geodesic Path" Tapped="MoveMapOnGeodesicPathBtn_Tapped" Height="45" Width="250"/>
                <Button Content="Draw Path" Tapped="DrawPathBtn_Tapped" Height="45" Width="250"/>
                <Button Content="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
{
    public sealed partial class MainPage : Page
    {
        private MapShapeLayer shapeLayer;

        private LocationCollection path = new LocationCollection(){
            new Location(42.8, 12.49),   //Italy
            new Location(51.5, 0),       //London
            new Location(40.8, -73.8),   //New York
            new Location(47.6, -122.3)   //Seattle
        };

        public MainPage()
        {
            this.InitializeComponent();

            shapeLayer = new MapShapeLayer();
            MyMap.ShapeLayers.Add(shapeLayer);
        }

        private void ClearMapBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
            ClearMap();
        }

        private void DropPinBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }

        private void BouncePinBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }

        private async void Bounce4PinsBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {   
        }

        private void MovePinOnPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }

        private void MovePinOnGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }

        private void MoveMapOnPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }

        private void MoveMapOnGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }

        private void DrawPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }

        private void DrawGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }

        private void ClearMap()
        {
            MyMap.Children.Clear();
            shapeLayer.Shapes.Clear();
        }
    }
}

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

 Imports Bing.Maps
Imports System.Threading.Tasks
Imports Windows.UI
Imports Windows.UI.Xaml.Controls
Imports Windows.UI.Xaml.Input
Imports Windows.UI.Xaml.Media.Animation

Public NotInheritable Class MainPage
    Inherits Page

    Private shapeLayer As MapShapeLayer

    'Italy
    'London
    'New York
    'Seattle
    Private path As New 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) _
    }

    Public Sub New()
        Me.InitializeComponent()

        shapeLayer = New MapShapeLayer()
        MyMap.ShapeLayers.Add(shapeLayer)
    End Sub

    Private Sub ClearMapBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
        ClearMap()
    End Sub

    Private Sub ScalingPinBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub DropPinBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub BouncePinBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Async Sub Bounce4PinsBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub MovePinOnPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub MovePinOnGeodesicPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub MoveMapOnPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub MoveMapOnGeodesicPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub DrawPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub DrawGeodesicPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub ClearMap()
        MyMap.Children.Clear()
        shapeLayer.Shapes.Clear()
    End Sub
End Class

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:

 <Application
    x:Class="AnimatedMaps.App"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AnimatedMaps">

    <Application.Resources>

        <!-- This storyboard will make the UIElement grow to double its size in 0.2 seconds -->
        <Storyboard x:Key="expandStoryboard">  
            <DoubleAnimation To="2" Duration="0:0:0.2" 
                             Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)"/>

            <DoubleAnimation To="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:

 private void 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:

 Private Sub ScalingPinBtn_Tapped(sender As Object, 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()
                                   End Sub

    AddHandler pin.PointerExited, Sub(s, a)
                                      story.Stop()
                                  End Sub

    MyMap.Children.Add(pin)
End Sub

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:

 <Storyboard x:Name="dropStoryboard">
    <DoubleAnimation From="-150" To="0" Duration="00:00:0.4" 
        Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)">
        <DoubleAnimation.EasingFunction>
            <QuadraticEase EasingMode="EaseIn"/>
        </DoubleAnimation.EasingFunction>
    </DoubleAnimation>
</Storyboard>

<Storyboard x:Name="bounceStoryboard">
    <DoubleAnimation From="-150" To="0" Duration="00:00:1" 
        Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)">
        <DoubleAnimation.EasingFunction>
            <BounceEase Bounces="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
{
    public static class PushpinAnimations
    {
        public static void 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();
        }

        public static void 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
            });
        }

        public static void 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.Maps
Imports Windows.Foundation
Imports Windows.UI.Xaml
Imports Windows.UI.Xaml.Media.Animation

Namespace AnimatedMaps.Animations
    Public NotInheritable Class PushpinAnimations

        Public Shared Sub AnimateY(pin As UIElement, fromY As Double, toY As Double, duration As Integer, 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()
        End Sub

        Public Shared Sub 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.Value

            Dim easing = New QuadraticEase()
            easing.EasingMode = EasingMode.EaseIn

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

        Public Shared Sub 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.Value
            
            Dim easing = New BounceEase()
            easing.Bounces = 2
            easing.EasingMode = EasingMode.EaseOut
            easing.Bounciness = 2

            AnimateY(pin, -from, -anchor.Y, duration, easing)
        End Sub
    End Class
End Namespace

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

 private void 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:

 Private Sub DropPinBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    ClearMap()

    Dim pin = New Pushpin()
    MapLayer.SetPosition(pin, MyMap.Center)

    MyMap.Children.Add(pin)

    AnimatedMaps.Animations.PushpinAnimations.Drop(pin, Nothing, Nothing)
End Sub

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:

 private void 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:

 Private Sub BouncePinBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    ClearMap()

    Dim pin = New Pushpin()
    MapLayer.SetPosition(pin, MyMap.Center)

    MyMap.Children.Add(pin)

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

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 As Object, e As TappedRoutedEventArgs)
    ClearMap()

    For i As Integer = 0 To path.Count - 1
        Dim pin = New Pushpin()
        MapLayer.SetPosition(pin, path(i))

        MyMap.Children.Add(pin)

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

        Await Task.Delay(500)
    Next
End Sub

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
{
    public class PathAnimation
    {
        private const int _delay = 30;
        private const double EARTH_RADIUS_KM = 6378.1;

        private DispatcherTimer _timerId;

        private LocationCollection _path;
        private bool _isGeodesic = false;

        private LocationCollection _intervalLocs;
        private List<int> _intervalIdx;

        private int? _duration;
        private int _frameIdx = 0;
        private bool _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++;
                }
            };
        }

        public delegate void IntervalCallback(Location loc, int pathIdx, int frameIdx);

        public LocationCollection Path
        {
            get { return _path; }
            set
            {
                _path = value;
                PreCalculate();
            }
        }

        public bool IsGeodesic
        {
            get { return _isGeodesic; }
            set 
            { 
                _isGeodesic = value;
                PreCalculate();
            }
        }

        public int? Duration
        {
            get { return _duration; }
            set
            {
                _duration = value;
                PreCalculate();
            }
        }

        public void Play()
        {
            _frameIdx = 0;
            _isPaused = false;
            _timerId.Start();
        }

        public void Pause()
        {
            _isPaused = true;
        }

        public void Stop()
        {
            if (_timerId.IsEnabled)
            {
                _frameIdx = 0;
                _isPaused = false;
                _timerId.Stop();
            }
        }

        private void PreCalculate()
        {
        }
    }
}

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

 Imports Bing.Maps
Imports System.Collections.Generic
Imports Windows.UI.Xaml

Namespace AnimatedMaps.Animations
    Public Class PathAnimation

        Private Const _delay As Integer = 30
        Private Const EARTH_RADIUS_KM As Double = 6378.1

        Private _timerId As DispatcherTimer

        Private _path As LocationCollection
        Private _isGeodesic As Boolean = False

        Private _intervalLocs As LocationCollection
        Private _intervalIdx As List(Of Integer)

        Private _duration As System.Nullable(Of Integer)
        Private _frameIdx As Integer = 0
        Private _isPaused As Boolean

        Public Sub New(path As LocationCollection, intervalCallback As IntervalCallback, isGeodesic As Boolean, 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)
                                          If Not _isPaused Then
                                              Dim progress As Double = CDbl(_frameIdx * _delay) / CDbl(_duration.Value)

                                              If progress > 1 Then
                                                  progress = 1
                                              End If

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

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

                                              _frameIdx += 1
                                          End If
                                      End Sub
        End Sub

        Public Delegate Sub IntervalCallback(loc As Location, pathIdx As Integer, frameIdx As Integer)

        Public Property Path() As LocationCollection
            Get
                Return _path
            End Get
            Set(value As LocationCollection)
                _path = value
                PreCalculate()
            End Set
        End Property

        Public Property IsGeodesic() As Boolean
            Get
                Return _isGeodesic
            End Get
            Set(value As Boolean)
                _isGeodesic = value
                PreCalculate()
            End Set
        End Property

        Public Property Duration() As System.Nullable(Of Integer)
            Get
                Return _duration
            End Get
            Set(value As System.Nullable(Of Integer))
                _duration = value
                PreCalculate()
            End Set
        End Property

        Public Sub Play()
            _frameIdx = 0
            _isPaused = False
            _timerId.Start()
        End Sub

        Public Sub Pause()
            _isPaused = True
        End Sub

        Public Sub [Stop]()
            If _timerId.IsEnabled Then
                _frameIdx = 0
                _isPaused = False
                _timerId.[Stop]()
            End If
        End Sub

        Private Sub PreCalculate()
        End Sub
    End Class
End Namespace

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.

 private static double DegToRad(double x)
{
    return x * Math.PI / 180;
}

private static double RadToDeg(double x)
{
    return x * 180 / Math.PI;
}

private static double 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;
}

private static double 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;
}

private static 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));

    return new Location(RadToDeg(lat2), RadToDeg(lon2));
}

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

 Private Shared Function DegToRad(x As Double) As Double
    Return x * Math.PI / 180
End Function

Private Shared Function RadToDeg(x As Double) As Double
    Return x * 180 / Math.PI
End Function

Private Shared Function HaversineDistance(origin As Location, dest As Location) As Double
    Dim lat1 As Double = DegToRad(origin.Latitude), lon1 As Double = DegToRad(origin.Longitude), lat2 As Double = DegToRad(dest.Latitude), lon2 As Double = DegToRad(dest.Longitude)

    Dim dLat As Double = lat2 - lat1, dLon As Double = lon2 - lon1, cordLength As Double = Math.Pow(Math.Sin(dLat / 2), 2) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Pow(Math.Sin(dLon / 2), 2), centralAngle As Double = 2 * Math.Atan2(Math.Sqrt(cordLength), Math.Sqrt(1 - cordLength))

    Return EARTH_RADIUS_KM * centralAngle
End Function

Private Shared Function CalculateBearing(origin As Location, dest As Location) As Double
    Dim lat1 = DegToRad(origin.Latitude)
    Dim lon1 = origin.Longitude
    Dim lat2 = DegToRad(dest.Latitude)
    Dim lon2 = dest.Longitude
    Dim 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 360
End Function

Private Shared Function CalculateCoord(origin As Location, brng As Double, arcLength As Double) As Location
    Dim lat1 As Double = DegToRad(origin.Latitude), lon1 As Double = DegToRad(origin.Longitude), centralAngle As Double = arcLength / EARTH_RADIUS_KM

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

    Return New Location(RadToDeg(lat2), RadToDeg(lon2))
End Function

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:

 private void PreCalculate()
{
    //Stop the timer
    if (_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:

 Private Sub PreCalculate()
    'Stop the timer
    If _timerId IsNot Nothing AndAlso _timerId.IsEnabled Then
        _timerId.[Stop]()
    End If

    _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 As Double, dlon As Double
    Dim totalDistance As Double = 0

    If _isGeodesic Then
        'Calcualte the total distance along the path in KM's.
        For i As Integer = 0 To _path.Count - 2
            totalDistance += HaversineDistance(_path(i), _path(i + 1))
        Next
    Else
        'Calcualte the total distance along the path in degrees.
        For i As Integer = 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)
        Next
    End If

    Dim frameCount As Integer = CInt(Math.Ceiling(CDbl(_duration.Value) / CDbl(_delay)))
    Dim idx As Integer = 0
    Dim progress As Double

    'Pre-calculate step points for smoother rendering.
    For f As Integer = 0 To frameCount - 1
        progress = CDbl(f * _delay) / CDbl(_duration.Value)

        Dim travel As Double = progress * totalDistance
        Dim alpha As Double = 0
        Dim dist As Double = 0
        Dim dx As Double = travel

        For i As Integer = 0 To _path.Count - 2

            If _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)
            End If

            If dist >= travel Then
                idx = i
                Exit For
            End If

            dx = travel - dist
        Next

        If dx <> 0 AndAlso idx < _path.Count - 1 Then
            If _isGeodesic Then
                Dim 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))
            End If

            _intervalIdx.Add(idx)
        End If
    Next

    'Ensure the last location is the last coordinate in the path.
    _intervalLocs.Add(_path(_path.Count - 1))
    _intervalIdx.Add(_path.Count - 1)
End Sub

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;

private void 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.PathAnimation

Private Sub ClearMap()
    MyMap.Children.Clear()
    shapeLayer.Shapes.Clear()

    If currentAnimation IsNot Nothing Then
        currentAnimation.Stop()
        currentAnimation = Nothing
    End If
End Sub

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.

 private void MovePinOnPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    MovePinOnPath(false);
}

private void MovePinOnGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    MovePinOnPath(true);
}

private void 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:

 Private Sub MovePinOnPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    MovePinOnPath(False)
End Sub

Private Sub MovePinOnGeodesicPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    MovePinOnPath(True)
End Sub

Private Sub MovePinOnPath(isGeodesic As Boolean)
    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)
            End Sub, isGeodesic, 10000)

    currentAnimation.Play()
End Sub

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.

 private void MoveMapOnPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    MoveMapOnPath(false);
}

private void MoveMapOnGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    MoveMapOnPath(true);
}

private void MoveMapOnPath(bool isGeodesic)
{
    ClearMap();

    //Change zooms levels as map reaches points along path.
    int[] zooms = new int[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:

 Private Sub MoveMapOnPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    MoveMapOnPath(False)
End Sub

Private Sub MoveMapOnGeodesicPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    MoveMapOnPath(True)
End Sub

Private Sub MoveMapOnPath(isGeodesic As Boolean)
    ClearMap()

    'Change zooms levels as map reaches points along path.
    Dim zooms As Integer() = New Integer(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))
        End Sub, isGeodesic, 10000)

    currentAnimation.Play()
End Sub

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.

 private void DrawPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    DrawPath(false);
}

private void DrawGeodesicPathBtn_Tapped(object sender, TappedRoutedEventArgs e)
{
    DrawPath(true);
}

private void 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);
        }
        else if (frameIdx > 1)
        {
            line.Locations.Add(coord);
        }
    }, isGeodesic, 10000);

    currentAnimation.Play();
}

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

 Private Sub DrawPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    DrawPath(False)
End Sub

Private Sub DrawGeodesicPathBtn_Tapped(sender As Object, e As TappedRoutedEventArgs)
    DrawPath(True)
End Sub

Private Sub DrawPath(isGeodesic As Boolean)
    ClearMap()

    Dim line As New 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)
                    End If
                End Sub, isGeodesic, 10000)

    currentAnimation.Play()
End Sub

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.

Comments