// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control // © 2020 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MapControl { /// /// Loads and optionally caches map tile images for a MapTileLayer. /// public partial class TileImageLoader : ITileImageLoader { /// /// Maximum number of parallel tile loading tasks. The default value is 4. /// public static int MaxLoadTasks { get; set; } = 4; /// /// 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 argument passed to LoadTilesAsync, /// the ZoomLevel, XIndex, and Y properties of a Tile, and the image file extension. /// The default value is "{0}/{1}/{2}/{3}{4}". /// public static string CacheKeyFormat { get; set; } = "{0}/{1}/{2}/{3}{4}"; private class TileQueue : ConcurrentStack { public void Enqueue(IEnumerable tiles) { PushRange(tiles.Reverse().ToArray()); } public bool TryDequeue(out Tile tile) { return TryPop(out tile); } } private readonly TileQueue tileQueue = new TileQueue(); private Func loadTileImage; private int taskCount; /// /// Loads all pending tiles from the tiles collection. /// If tileSource.UriFormat starts with "http" and sourceName is a non-empty string, /// tile images will be cached in the TileImageLoader's Cache (if it's not null). /// The method is async void because it implements void ITileImageLoader.LoadTiles /// and is not awaited when it is called in MapTileLayer.UpdateTiles(). /// public async void LoadTiles(IEnumerable tiles, TileSource tileSource, string sourceName) { tileQueue.Clear(); tiles = tiles.Where(tile => tile.Pending); if (tiles.Any() && tileSource != null) { if (Cache != null && tileSource.UriFormat != null && tileSource.UriFormat.StartsWith("http") && !string.IsNullOrEmpty(sourceName)) { loadTileImage = tile => LoadCachedTileImageAsync(tile, tileSource, sourceName); } else { loadTileImage = tile => LoadTileImageAsync(tile, tileSource); } tileQueue.Enqueue(tiles); var numTasks = Math.Min(tileQueue.Count, MaxLoadTasks); var tasks = Enumerable.Range(0, numTasks).Select(n => LoadTilesFromQueueAsync()); await Task.WhenAll(tasks).ConfigureAwait(false); } } private async Task LoadTilesFromQueueAsync() { if (Interlocked.Increment(ref taskCount) <= MaxLoadTasks) { while (tileQueue.TryDequeue(out Tile tile)) { tile.Pending = false; try { Debug.WriteLine(Thread.CurrentThread.ManagedThreadId); await loadTileImage(tile).ConfigureAwait(false); } catch (Exception ex) { Debug.WriteLine("TileImageLoader: {0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message); } } } Interlocked.Decrement(ref taskCount); } private static 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).ConfigureAwait(false); } } private static DateTime GetExpiration(TimeSpan? maxAge) { return DateTime.UtcNow.Add(maxAge ?? DefaultCacheExpiration); } } }