I am using the Telerik map, I am driving the map with custom controls, that I have on a timer update function
When I move the map struggles to catch up & becomes quite jerky. I need to drive the map this way I don't have a choice in this. Using the Telerik controls to drive it is not an option unfortunately.
From what I can tell it looks like the map or the provider is blocking the UI thread which causes the stall.
This bug is more apparent in high resolutions.
Below is the code I am using to generate the error (I know the code isn't perfect but it highlights the bug)
turn these defines on to change timer methods (1 at a time please) or turn them both off to use the onIdle event
When I move the map struggles to catch up & becomes quite jerky. I need to drive the map this way I don't have a choice in this. Using the Telerik controls to drive it is not an option unfortunately.
From what I can tell it looks like the map or the provider is blocking the UI thread which causes the stall.
This bug is more apparent in high resolutions.
Below is the code I am using to generate the error (I know the code isn't perfect but it highlights the bug)
turn these defines on to change timer methods (1 at a time please) or turn them both off to use the onIdle event
#define DISPACTTIMER #define THREADTIMER
<Window x:Class="WpfRadMap.MainWindow" Title="MainWindow" Height="1080" Width="1900" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" ResizeMode="NoResize" Activated="Window_Activated"> <Grid> <telerik:RadMap Panel.ZIndex="1" HorizontalAlignment="Stretch" Name="radMap1" VerticalAlignment="Stretch" Center="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Mode=TwoWay, Path=MapLocation}" ZoomChanged="radMap1_ZoomChanged"/> <Canvas Panel.ZIndex="2" x:Name="gizmoPanel" Visibility="Visible" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RenderTransformOrigin="0.5,0.5" > <Canvas.RenderTransform> <RotateTransform x:Name="GizmoRotate" Angle="0" /> </Canvas.RenderTransform> <Line Stroke="Red" StrokeThickness="3" ClipToBounds="True" X1="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoX1}" X2="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoX1}" Y1="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoY1}" Y2="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoY2}" Visibility="Visible" IsHitTestVisible="False" > <Line.Effect> <DropShadowEffect BlurRadius="5" ShadowDepth="0" /> </Line.Effect> </Line> <!-- Small Line 1 --> <Line Stroke="Red" StrokeThickness="3" ClipToBounds="True" X1="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoX1}" X2="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoX2}" Y1="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoY2}" Y2="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoY3}" Visibility="Visible" IsHitTestVisible="False" > <Line.Effect> <DropShadowEffect BlurRadius="5" ShadowDepth="0" /> </Line.Effect> </Line> <!-- Small Line 2 --> <Line Stroke="Red" StrokeThickness="3" ClipToBounds="True" X1="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoX1}" X2="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoX3}" Y1="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoY2}" Y2="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=GizmoY3}" Visibility="Visible" IsHitTestVisible="False" > <Line.Effect> <DropShadowEffect BlurRadius="5" ShadowDepth="0" /> </Line.Effect> </Line> </Canvas> <Button Panel.ZIndex="3" Content="Rotate Clockwise" Height="39" Name="button1" Width="99" Click="button1_Click" Margin="1690,12,39,980" /> <Button Panel.ZIndex="3" Content="Rotate AntiClockwise" Height="39" Name="button2" Width="119" Click="button2_Click" Margin="1680,57,28,935" /> <Label Panel.ZIndex="3" Margin="1670,100, 0,910" Content="[W] to move along arrow" Height="28" Name="label1" Width="180" FontWeight="Bold" /> <Label Panel.ZIndex="3" Margin="1670,120, 0,890" Content="[S] to move opposite to arrow" Height="28" Name="label2" Width="180" FontWeight="Bold" /> <Label Panel.ZIndex="3" Margin="1670,140, 0,870" Content="[A] to move left of arrow" Height="28" Name="label3" Width="180" FontWeight="Bold" /> <Label Panel.ZIndex="3" Margin="1670,160, 0,850" Content="[D] to move right of arrow" Height="28" Name="label4" Width="180" FontWeight="Bold" /> <Label Panel.ZIndex="3" Margin="0,10, 1670,980" Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=FastestTime}" Height="28" Name="lblMinSpeed" Width="180" FontWeight="Bold" /> <Label Panel.ZIndex="3" Margin="0,40, 1670,950" Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=SlowestTime}" Height="28" Name="lblMaxSpeed" Width="180" FontWeight="Bold" /> <Label Panel.ZIndex="3" Margin="0,70, 1670,920" Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=AverageTime}" Height="28" Name="lblAvgSpeed" Width="180" FontWeight="Bold" /> <Label Panel.ZIndex="3" Margin="0,110, 1670,880" Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=FastestUpdateTime}" Height="28" Name="lblMinUpdate" Width="180" FontWeight="Bold" /> <Label Panel.ZIndex="3" Margin="0,140, 1670,850" Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=SlowestUpdateTime}" Height="28" Name="lblMaxUpdate" Width="180" FontWeight="Bold" /> <Label Panel.ZIndex="3" Margin="0,170, 1670,820" Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=AverageUpdateTime}" Height="28" Name="lblAvgUpdate" Width="180" FontWeight="Bold" /> </Grid></Window>#define QUERY_PERFORMANCE //#define DISPACTTIMER//#define THREADTIMERusing System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Windows;using System.Windows.Controls;using System.Windows.Data;using System.Windows.Documents;using System.Windows.Input;using System.Windows.Media;using System.Windows.Media.Imaging;using System.Windows.Navigation;using System.Windows.Shapes;using System.Windows.Threading;using System.Timers;using System.ComponentModel;namespace WpfRadMap{ /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window, INotifyPropertyChanged { Telerik.Windows.Controls.Map.Location m_location; double width = 1900.0; double height = 1080.0; private double gizmoX1; private double gizmoX2; private double gizmoX3; private double gizmoY1; private double gizmoY2; private double gizmoY3; private RotateTransform longLatRotate; private double framesPerSecond = 60.0;#if DISPACTTIMER DispatcherTimer m_updateTimer;#elif THREADTIMER Timer m_updateTimer;#endif public MainWindow() { gizmoX1 = width * 0.5; gizmoX2 = width * 0.485; gizmoX3 = width * 0.515; gizmoY1 = height * 0.5; gizmoY2 = height * 0.45; gizmoY3 = height * 0.475; longLatRotate = new RotateTransform(); longLatRotate.Angle = 0.0; m_location = new Telerik.Windows.Controls.Map.Location(); InitializeComponent(); const string bingKey = ""; if (bingKey.Length > 0) { Telerik.Windows.Controls.Map.BingMapProvider bingProvider = new Telerik.Windows.Controls.Map.BingMapProvider(Telerik.Windows.Controls.Map.MapMode.Aerial, true, bingKey); bingProvider.IsTileCachingEnabled = true; bingProvider.IsLabelVisible = true; radMap1.Provider = bingProvider; SetLabelsFontColor(Brushes.White); } else { Telerik.Windows.Controls.Map.OpenStreetMapProvider openProvider = new Telerik.Windows.Controls.Map.OpenStreetMapProvider(); openProvider.IsTileCachingEnabled = true; radMap1.Provider = openProvider; } radMap1.ZoomLevel = 19; m_location.Latitude = 48.856941702106354; m_location.Longitude = 2.3383069038354383; MapLocation = m_location;#if DISPACTTIMER m_updateTimer = new DispatcherTimer(); m_updateTimer.Tick += new EventHandler(m_updateTimer_Tick); m_updateTimer.Interval = TimeSpan.FromSeconds(1.0/framesPerSecond);#elif THREADTIMER m_updateTimer = new Timer(); m_updateTimer.Elapsed += new ElapsedEventHandler(m_updateTimer_Tick); m_updateTimer.Interval = (1.0 / framesPerSecond) * 100; m_updateTimer.Enabled = false;#else System.Windows.Interop.ComponentDispatcher.ThreadIdle += new System.EventHandler(ComponentDispatcher_ThreadIdle);#endif profileTimer = System.Diagnostics.Stopwatch.StartNew(); } public Telerik.Windows.Controls.Map.Location MapLocation { get { return m_location; } set { m_location = value; RaisePropertyChanged("MapLocation"); } } double m_mapHeight = 0.0001; public double MapHeight { get {return m_mapHeight;} set {m_mapHeight = value; RaisePropertyChanged("MapHeight");} } private void radMap1_ZoomChanged(object sender, EventArgs e) { MapHeight = radMap1.GeographicalBounds.Height; } readonly Queue<long> m_frameTicks = new Queue<long>(); long m_slowest = long.MinValue; long m_fastest = long.MaxValue; long m_avg = 0; public string AverageTime { get { return "Avg: " + m_avg + "us"; } } public string SlowestTime { get { return "Slowest: " + m_slowest + "us"; } } public string FastestTime { get { return "Fastest: " + m_fastest + "us"; } } readonly Queue<long> m_frameUpdateTicks = new Queue<long>(); long m_slowestUpdate = long.MinValue; long m_fastestUpdate = long.MaxValue; long m_avgUpdate = 0; System.Diagnostics.Stopwatch profileTimer; public string AverageUpdateTime { get { return "AvgUpdate: " + m_avgUpdate + "ms"; } } public string SlowestUpdateTime { get { return "SlowestUpdate: " + m_slowestUpdate + "ms"; } } public string FastestUpdateTime { get { return "FastestUpdate: " + m_fastestUpdate + "ms"; } } protected void CalculatePerformance(long a_currentTick, Queue<long> a_avgTicks, ref long a_slowestTick, ref long a_fastestTick, ref long a_avgTick) { a_avgTicks.Enqueue(a_currentTick); if (a_avgTicks.Count > 60) { a_avgTicks.Dequeue(); } long slowest = a_currentTick; long fastest = a_currentTick; long total = 0; foreach (long time in a_avgTicks) { total += time; slowest = Math.Max(slowest, time); fastest = Math.Min(fastest, time); } a_avgTick = total / a_avgTicks.Count; a_slowestTick = slowest; a_fastestTick = fastest; }#if DISPACTTIMER void m_updateTimer_Tick(object sender, EventArgs e)#elif THREADTIMER void m_updateTimer_Tick(object sender, ElapsedEventArgs e)#else void ComponentDispatcher_ThreadIdle(object sender, EventArgs e)#endif { long elapsedTime = profileTimer.ElapsedTicks; long elapsedMilliSeconds = System.Convert.ToInt64((System.Convert.ToDouble(elapsedTime) / System.Convert.ToDouble(System.Diagnostics.Stopwatch.Frequency)) * 1000);// if (elapsedMilliSeconds < (1.0/framesPerSecond)*1000)// {// return;// } CalculatePerformance(elapsedMilliSeconds, m_frameUpdateTicks, ref m_slowestUpdate, ref m_fastestUpdate, ref m_avgUpdate); RaisePropertyChanged("SlowestUpdateTime"); RaisePropertyChanged("FastestUpdateTime"); RaisePropertyChanged("AverageUpdateTime"); profileTimer.Restart(); double tearRepair = 1.0; if (elapsedMilliSeconds > 0) { tearRepair = elapsedMilliSeconds / framesPerSecond; } double speed = 2.0; double latitude = m_location.Latitude; double longitude = m_location.Longitude;#if THREADTIMER Point stepDir = new Point(0.0, (MapHeight/(speed * framesPerSecond))*tearRepair); Point dir = stepDir; //longLatRotate.Transform(stepDir); latitude += dir.Y; longitude -= dir.X;#else#if DISPACTTIMER Point stepDir = new Point(0.0, (MapHeight/(speed * framesPerSecond))*tearRepair);#else Point stepDir = new Point(0.0, (MapHeight / (speed * framesPerSecond)) * tearRepair);#endif Point dir = longLatRotate.Transform(stepDir); if ((Keyboard.GetKeyStates(Key.W) & KeyStates.Down) > 0) { latitude += dir.Y; longitude -= dir.X; } if ((Keyboard.GetKeyStates(Key.S) & KeyStates.Down) > 0) { latitude -= dir.Y; longitude += dir.X; } if ((Keyboard.GetKeyStates(Key.A) & KeyStates.Down) > 0) { latitude -= dir.X; longitude -= dir.Y; } if ((Keyboard.GetKeyStates(Key.D) & KeyStates.Down) > 0) { latitude += dir.X; longitude += dir.Y; }#endif while (latitude>360) { latitude-=360; } while(latitude < 0) { latitude += 360; } while(longitude > 360) { longitude -= 360; } while(longitude < 0) { longitude += 360; } m_location.Latitude = latitude; m_location.Longitude = longitude; RaisePropertyChanged("MapLocation"); elapsedTime = profileTimer.ElapsedTicks; long elapsedMicroSeconds = System.Convert.ToInt64((System.Convert.ToDouble(elapsedTime) / System.Convert.ToDouble(System.Diagnostics.Stopwatch.Frequency))*1000000); CalculatePerformance(elapsedMicroSeconds, m_frameTicks, ref m_slowest, ref m_fastest, ref m_avg); RaisePropertyChanged("SlowestTime"); RaisePropertyChanged("FastestTime"); RaisePropertyChanged("AverageTime"); } public double GizmoX1 { get { return gizmoX1; } set { gizmoX1 = value; } } public double GizmoX2 { get { return gizmoX2; } set { gizmoX2 = value; } } public double GizmoX3 { get { return gizmoX3; } set { gizmoX3 = value; } } public double GizmoY1 { get { return gizmoY1; } set { gizmoY1 = value; } } public double GizmoY2 { get { return gizmoY2; } set { gizmoY2 = value; } } public double GizmoY3 { get { return gizmoY3; } set { gizmoY3 = value; } } private void button2_Click(object sender, RoutedEventArgs e) { longLatRotate.Angle = GizmoRotate.Angle = (GizmoRotate.Angle + 15) % 360; } private void button1_Click(object sender, RoutedEventArgs e) { longLatRotate.Angle = GizmoRotate.Angle = (GizmoRotate.Angle - 15); if (GizmoRotate.Angle == 0) { longLatRotate.Angle = GizmoRotate.Angle = 360; } } void SetLabelsFontColor(Brush a_color) { lblMinUpdate.Foreground = a_color; lblMaxUpdate.Foreground = a_color; lblAvgUpdate.Foreground = a_color; lblMinSpeed.Foreground = a_color; lblMaxSpeed.Foreground = a_color; lblAvgSpeed.Foreground = a_color; label1.Foreground = a_color; label2.Foreground = a_color; label3.Foreground = a_color; label4.Foreground = a_color; } /// <summary> /// Raises this object's PropertyChanged event. /// </summary> /// <param name="propertyName">The property that has a new value.</param> protected void RaisePropertyChanged(string propertyName) { this.VerifyPropertyName(propertyName); PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName); handler(this, e); } } /// <summary> /// Warns the developer if this object does not have /// a public property with the specified name. This /// method does not exist in a Release build. /// </summary> public void VerifyPropertyName(string propertyName) { // If you raise PropertyChanged and do not specify a property name, // all properties on the object are considered to be changed by the binding system. if (String.IsNullOrEmpty(propertyName)) { return; } // Verify that the property name matches a real, // public, instance property on this object. if (TypeDescriptor.GetProperties(this)[propertyName] == null) { string msg = "Invalid property name: " + propertyName; if (this.ThrowOnInvalidPropertyName) { throw new ArgumentException(msg); } } } /// <summary> /// Returns whether an exception is thrown, or if a Debug.Fail() is used /// when an invalid property name is passed to the VerifyPropertyName method. /// The default value is false, but subclasses used by unit tests might /// override this property's getter to return true. /// </summary> protected virtual bool ThrowOnInvalidPropertyName { get; private set; } /// <summary> /// Raised when a property on this object has a new value. /// </summary> public event PropertyChangedEventHandler PropertyChanged; private void Window_Activated(object sender, EventArgs e) { MapHeight = radMap1.GeographicalBounds.Height;#if DISPACTTIMER m_updateTimer.Start();#elif THREADTIMER m_updateTimer.Enabled = true;#endif } }}