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
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.
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
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
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:
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 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 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 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
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 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 pin
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
- Anonymous
November 26, 2014
For fast responses to questions try using the Bing Maps forums: social.msdn.microsoft.com/.../home If you are creating a Windows Store app then take a look at my free eBook: rbrundritt.wordpress.com/my-book