// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control // © 2022 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.Globalization; 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 { private class TileQueue : ConcurrentStack { public TileQueue(IEnumerable tiles) : base(tiles.Where(tile => tile.Pending).Reverse()) { } public bool IsCanceled { get; private set; } public bool TryDequeue(out Tile tile) { tile = null; if (IsCanceled || !TryPop(out tile)) { return false; } tile.Pending = false; return true; } public void Cancel() { IsCanceled = true; Clear(); } } /// /// 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); /// /// Maximum expiration time for cached tile images. A transmitted expiration time /// that exceeds this value is ignored. The default value is ten days. /// public static TimeSpan MaxCacheExpiration { get; set; } = TimeSpan.FromDays(10); /// /// Reports tile loading process as double value between 0 and 1. /// public IProgress Progress { get; set; } /// /// The current TileSource, passed to the most recent LoadTiles call. /// public TileSource TileSource { get; private set; } private TileQueue pendingTiles; private int progressTotal; private int progressLoaded; /// /// Loads all pending tiles from the tiles collection. /// If tileSource.UriFormat starts with "http" and cacheName is a non-empty string, /// tile images will be cached in the TileImageLoader's Cache - if that is not null. /// public Task LoadTiles(IEnumerable tiles, TileSource tileSource, string cacheName) { pendingTiles?.Cancel(); TileSource = tileSource; if (tileSource != null) { pendingTiles = new TileQueue(tiles); var numTasks = Math.Min(pendingTiles.Count, MaxLoadTasks); if (numTasks > 0) { if (Progress != null) { progressTotal = pendingTiles.Count; progressLoaded = 0; Progress.Report(0d); } if (Cache == null || tileSource.UriFormat == null || !tileSource.UriFormat.StartsWith("http")) { cacheName = null; // no tile caching } return Task.WhenAll(Enumerable.Range(0, numTasks).Select( _ => Task.Run(() => LoadPendingTiles(pendingTiles, tileSource, cacheName)))); } } if (Progress != null && progressLoaded < progressTotal) { Progress.Report(1d); } return Task.CompletedTask; } private async Task LoadPendingTiles(TileQueue tileQueue, TileSource tileSource, string cacheName) { while (tileQueue.TryDequeue(out var tile)) { try { await LoadTile(tile, tileSource, cacheName).ConfigureAwait(false); } catch (Exception ex) { Debug.WriteLine($"TileImageLoader: {tile.ZoomLevel}/{tile.XIndex}/{tile.Y}: {ex.Message}"); } if (Progress != null && !tileQueue.IsCanceled) { Interlocked.Increment(ref progressLoaded); Progress.Report((double)progressLoaded / progressTotal); } } } private static Task LoadTile(Tile tile, TileSource tileSource, string cacheName) { if (string.IsNullOrEmpty(cacheName)) { return LoadTile(tile, tileSource); } 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(CultureInfo.InvariantCulture, "{0}/{1}/{2}/{3}{4}", cacheName, tile.ZoomLevel, tile.XIndex, tile.Y, extension); return LoadCachedTile(tile, uri, cacheKey); } return Task.CompletedTask; } private static DateTime GetExpiration(TimeSpan? maxAge) { if (!maxAge.HasValue) { maxAge = DefaultCacheExpiration; } else if (maxAge.Value > MaxCacheExpiration) { maxAge = MaxCacheExpiration; } return DateTime.UtcNow.Add(maxAge.Value); } } }