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.
Add a reference to the Bing Maps SDK. To do this, right-click the References folder and press Add Reference. Select Windows → Extensions, 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.
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.
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="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"> <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:
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="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://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:
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:
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:
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:
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.
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.
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.
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