diff --git a/MapControl/Shared/MapTileLayer.cs b/MapControl/Shared/MapTileLayer.cs index 676cb21b..69d5f917 100644 --- a/MapControl/Shared/MapTileLayer.cs +++ b/MapControl/Shared/MapTileLayer.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; #if WINDOWS_UWP using Windows.Foundation; using Windows.UI.Xaml; @@ -22,7 +21,7 @@ namespace MapControl { public interface ITileImageLoader { - Task LoadTilesAsync(MapTileLayer tileLayer); + void LoadTilesAsync(MapTileLayer tileLayer); } /// @@ -93,7 +92,6 @@ namespace MapControl IsHitTestVisible = false; RenderTransform = new MatrixTransform(); TileImageLoader = tileImageLoader; - Tiles = new List(); updateTimer = new DispatcherTimer { Interval = UpdateInterval }; updateTimer.Tick += (s, e) => UpdateTileGrid(); @@ -102,9 +100,11 @@ namespace MapControl } public ITileImageLoader TileImageLoader { get; private set; } - public ICollection Tiles { get; private set; } + public TileGrid TileGrid { get; private set; } + public IReadOnlyCollection Tiles { get; private set; } = new List(); + /// /// Provides map tile URIs or images. /// @@ -273,7 +273,7 @@ namespace MapControl { if (TileGrid != null) { - Tiles.Clear(); + Tiles = new List(); UpdateTiles(); } } @@ -344,9 +344,7 @@ namespace MapControl var maxZoomLevel = Math.Min(TileGrid.ZoomLevel, MaxZoomLevel); var minZoomLevel = MinZoomLevel; - if (minZoomLevel < maxZoomLevel && - parentMap.MapLayer != this && - parentMap.Children.Cast().FirstOrDefault() != this) + if (minZoomLevel < maxZoomLevel && parentMap.MapLayer != this) { minZoomLevel = maxZoomLevel; // do not load lower level tiles if this is note a "base" layer } @@ -393,7 +391,7 @@ namespace MapControl Children.Add(tile.Image); } - var task = TileImageLoader.LoadTilesAsync(this); + TileImageLoader.LoadTilesAsync(this); } } } diff --git a/MapControl/Shared/PolygonCollection.cs b/MapControl/Shared/PolygonCollection.cs index 084433f2..3ed971f9 100644 --- a/MapControl/Shared/PolygonCollection.cs +++ b/MapControl/Shared/PolygonCollection.cs @@ -18,6 +18,12 @@ namespace MapControl /// public class PolygonCollection : ObservableCollection>, IWeakEventListener { + public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender)); + return true; + } + protected override void InsertItem(int index, IEnumerable polygon) { var observablePolygon = polygon as INotifyCollectionChanged; @@ -63,12 +69,5 @@ namespace MapControl base.ClearItems(); } - - bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) - { - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender)); - - return true; - } } } diff --git a/MapControl/Shared/TileImageLoader.cs b/MapControl/Shared/TileImageLoader.cs index 30df2cd2..46f2c181 100644 --- a/MapControl/Shared/TileImageLoader.cs +++ b/MapControl/Shared/TileImageLoader.cs @@ -18,20 +18,25 @@ namespace MapControl public partial class TileImageLoader : ITileImageLoader { /// - /// Default expiration time for cached tile images. Used when no expiration time - /// was transmitted on download. The default value is one day. + /// Maximum number of parallel tile loading tasks. The default value is 4. /// - public static TimeSpan DefaultCacheExpiration { get; set; } = TimeSpan.FromDays(1); + public static int MaxLoadTasks { get; set; } = 4; /// /// Minimum expiration time for cached tile images. The default value is one hour. /// - public static TimeSpan MinimumCacheExpiration { get; set; } = TimeSpan.FromHours(1); + public static TimeSpan MinCacheExpiration { get; set; } = TimeSpan.FromHours(1); /// /// Maximum expiration time for cached tile images. The default value is one week. /// - public static TimeSpan MaximumCacheExpiration { get; set; } = TimeSpan.FromDays(7); + public static TimeSpan MaxCacheExpiration { get; set; } = TimeSpan.FromDays(7); + + /// + /// Default expiration time for cached tile images. Used when no expiration time + /// was transmitted on download. The default value is one day. + /// + public static TimeSpan DefaultCacheExpiration { get; set; } = TimeSpan.FromDays(1); /// /// Format string for creating cache keys from the SourceName property of a TileSource, @@ -43,7 +48,12 @@ namespace MapControl private readonly ConcurrentStack pendingTiles = new ConcurrentStack(); private int taskCount; - public async Task LoadTilesAsync(MapTileLayer tileLayer) + /// + /// Loads all pending tiles from the Tiles collection of a MapTileLayer by running up to MaxLoadTasks parallel Tasks. + /// If the TileSource's SourceName is non-empty and its UriFormat starts with "http", tile images are cached in the + /// TileImageLoader's Cache. + /// + public void LoadTilesAsync(MapTileLayer tileLayer) { pendingTiles.Clear(); @@ -53,55 +63,34 @@ namespace MapControl if (tileSource != null && tiles.Any()) { - if (Cache == null || string.IsNullOrEmpty(sourceName) || - tileSource.UriFormat == null || !tileSource.UriFormat.StartsWith("http")) - { - // no caching, load tile images directly + pendingTiles.PushRange(tiles.Reverse().ToArray()); - foreach (var tile in tiles) - { - await LoadTileImageAsync(tileSource, tile); - } + Func loadFunc; + + if (Cache != null && !string.IsNullOrEmpty(sourceName) && + tileSource.UriFormat != null && tileSource.UriFormat.StartsWith("http")) + { + loadFunc = tile => LoadCachedTileImageAsync(tile, tileSource, sourceName); } else { - pendingTiles.PushRange(tiles.Reverse().ToArray()); - - while (taskCount < Math.Min(pendingTiles.Count, DefaultConnectionLimit)) - { - Interlocked.Increment(ref taskCount); - - var task = Task.Run(async () => // do not await - { - await LoadPendingTilesAsync(tileSource, sourceName); // run multiple times in parallel - - Interlocked.Decrement(ref taskCount); - }); - } + loadFunc = tile => LoadTileImageAsync(tile, tileSource); } - } - } - private async Task LoadTileImageAsync(TileSource tileSource, Tile tile) - { - tile.Pending = false; + var maxTasks = Math.Min(pendingTiles.Count, MaxLoadTasks); - try - { - var imageSource = await tileSource.LoadImageAsync(tile.XIndex, tile.Y, tile.ZoomLevel); - - if (imageSource != null) + while (taskCount < maxTasks) { - tile.SetImage(imageSource); + Interlocked.Increment(ref taskCount); + + var task = Task.Run(() => LoadTilesAsync(loadFunc)); // do not await } - } - catch (Exception ex) - { - Debug.WriteLine("TileImageLoader: {0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message); + + //Debug.WriteLine("{0}: {1} tasks", Environment.CurrentManagedThreadId, taskCount); } } - private async Task LoadPendingTilesAsync(TileSource tileSource, string sourceName) + private async Task LoadTilesAsync(Func loadTileImageFunc) { Tile tile; @@ -111,27 +100,35 @@ namespace MapControl try { - var uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel); - - if (uri != null) - { - var extension = Path.GetExtension(uri.LocalPath); - - if (string.IsNullOrEmpty(extension) || extension == ".jpeg") - { - extension = ".jpg"; - } - - var cacheKey = string.Format(CacheKeyFormat, sourceName, tile.ZoomLevel, tile.XIndex, tile.Y, extension); - - await LoadTileImageAsync(tile, uri, cacheKey); - } + await loadTileImageFunc(tile); } catch (Exception ex) { Debug.WriteLine("TileImageLoader: {0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message); } } + + Interlocked.Decrement(ref taskCount); + //Debug.WriteLine("{0}: {1} tasks", Environment.CurrentManagedThreadId, taskCount); + } + + private async Task LoadCachedTileImageAsync(Tile tile, TileSource tileSource, string sourceName) + { + var uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel); + + if (uri != null) + { + var extension = Path.GetExtension(uri.LocalPath); + + if (string.IsNullOrEmpty(extension) || extension == ".jpeg") + { + extension = ".jpg"; + } + + var cacheKey = string.Format(CacheKeyFormat, sourceName, tile.ZoomLevel, tile.XIndex, tile.Y, extension); + + await LoadCachedTileImageAsync(tile, uri, cacheKey); + } } private static DateTime GetExpiration(TimeSpan? maxAge) @@ -142,13 +139,13 @@ namespace MapControl { expiration = maxAge.Value; - if (expiration < MinimumCacheExpiration) + if (expiration < MinCacheExpiration) { - expiration = MinimumCacheExpiration; + expiration = MinCacheExpiration; } - else if (expiration > MaximumCacheExpiration) + else if (expiration > MaxCacheExpiration) { - expiration = MaximumCacheExpiration; + expiration = MaxCacheExpiration; } } diff --git a/MapControl/UWP/TileImageLoader.UWP.cs b/MapControl/UWP/TileImageLoader.UWP.cs index bbed6327..94527f46 100644 --- a/MapControl/UWP/TileImageLoader.UWP.cs +++ b/MapControl/UWP/TileImageLoader.UWP.cs @@ -26,12 +26,7 @@ namespace MapControl /// public static Caching.IImageCache Cache { get; set; } - /// - /// Gets or sets the maximum number of concurrent connections. The default value is 2. - /// - public static int DefaultConnectionLimit { get; set; } = 2; - - private async Task LoadTileImageAsync(Tile tile, Uri uri, string cacheKey) + private async Task LoadCachedTileImageAsync(Tile tile, Uri uri, string cacheKey) { var cacheItem = await Cache.GetAsync(cacheKey); var cacheBuffer = cacheItem?.Buffer; @@ -46,7 +41,7 @@ namespace MapControl if (result.Item1 != null) // tile image available { - await SetTileImageAsync(tile, result.Item1); // show before caching + await LoadTileImageAsync(tile, result.Item1); await Cache.SetAsync(cacheKey, result.Item1, GetExpiration(result.Item2)); } } @@ -54,21 +49,20 @@ namespace MapControl if (cacheBuffer != null) { - await SetTileImageAsync(tile, cacheBuffer); + await LoadTileImageAsync(tile, cacheBuffer); } } - private async Task SetTileImageAsync(Tile tile, IBuffer buffer) + private async Task LoadTileImageAsync(Tile tile, IBuffer buffer) { var tcs = new TaskCompletionSource(); using (var stream = new InMemoryRandomAccessStream()) { await stream.WriteAsync(buffer); - await stream.FlushAsync(); // necessary? stream.Seek(0); - await tile.Image.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => + await tile.Image.Dispatcher.RunAsync(CoreDispatcherPriority.Low, async () => { try { @@ -84,5 +78,25 @@ namespace MapControl await tcs.Task; } + + private async Task LoadTileImageAsync(Tile tile, TileSource tileSource) + { + var tcs = new TaskCompletionSource(); + + await tile.Image.Dispatcher.RunAsync(CoreDispatcherPriority.Low, async () => + { + try + { + tile.SetImage(await tileSource.LoadImageAsync(tile.XIndex, tile.Y, tile.ZoomLevel)); + tcs.SetResult(null); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }); + + await tcs.Task; + } } } diff --git a/MapControl/WPF/MapShape.WPF.cs b/MapControl/WPF/MapShape.WPF.cs index 4e887eec..cca7b456 100644 --- a/MapControl/WPF/MapShape.WPF.cs +++ b/MapControl/WPF/MapShape.WPF.cs @@ -21,7 +21,7 @@ namespace MapControl get { return Data; } } - bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { UpdateData(); return true; diff --git a/MapControl/WPF/TileImageLoader.WPF.cs b/MapControl/WPF/TileImageLoader.WPF.cs index bb128266..2c42aee9 100644 --- a/MapControl/WPF/TileImageLoader.WPF.cs +++ b/MapControl/WPF/TileImageLoader.WPF.cs @@ -4,10 +4,11 @@ using System; using System.IO; -using System.Net; using System.Runtime.Caching; using System.Text; using System.Threading.Tasks; +using System.Windows.Media; +using System.Windows.Threading; namespace MapControl { @@ -27,12 +28,7 @@ namespace MapControl /// public static ObjectCache Cache { get; set; } = MemoryCache.Default; - private static int DefaultConnectionLimit - { - get { return ServicePointManager.DefaultConnectionLimit; } - } - - private async Task LoadTileImageAsync(Tile tile, Uri uri, string cacheKey) + private async Task LoadCachedTileImageAsync(Tile tile, Uri uri, string cacheKey) { DateTime expiration; var cacheBuffer = GetCachedImage(cacheKey, out expiration); @@ -49,7 +45,7 @@ namespace MapControl { using (var stream = result.Item1) { - SetTileImage(tile, stream); // show before caching + LoadTileImage(tile, stream); SetCachedImage(cacheKey, stream, GetExpiration(result.Item2)); } } @@ -60,15 +56,23 @@ namespace MapControl { using (var stream = new MemoryStream(cacheBuffer)) { - SetTileImage(tile, stream); + LoadTileImage(tile, stream); } } } - private void SetTileImage(Tile tile, Stream stream) + private async Task LoadTileImageAsync(Tile tile, TileSource tileSource) { - var imageSource = ImageLoader.LoadImage(stream); + SetTileImage(tile, await tileSource.LoadImageAsync(tile.XIndex, tile.Y, tile.ZoomLevel)); + } + private void LoadTileImage(Tile tile, Stream stream) + { + SetTileImage(tile, ImageLoader.LoadImage(stream)); + } + + private void SetTileImage(Tile tile, ImageSource imageSource) + { tile.Image.Dispatcher.InvokeAsync(() => tile.SetImage(imageSource)); }