// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control // © 2017 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; using Windows.Storage; using Windows.Storage.Streams; using Windows.UI.Core; using Windows.UI.Xaml.Media.Imaging; using Windows.Web.Http; using Windows.Web.Http.Filters; namespace MapControl { /// /// Loads map tile images and optionally caches them in a IImageCache. /// public class TileImageLoader : ITileImageLoader { /// /// Default name of an IImageCache instance that is assigned to the Cache property. /// public const string DefaultCacheName = "TileCache"; /// /// Default StorageFolder where an IImageCache instance may save cached data. /// public static readonly StorageFolder DefaultCacheFolder = ApplicationData.Current.TemporaryFolder; /// /// 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; } /// /// Minimum expiration time for cached tile images. Used when an unnecessarily small expiration time /// was transmitted on download (e.g. Cache-Control: max-age=0). The default value is one hour. /// public static TimeSpan MinimumCacheExpiration { get; set; } /// /// The IImageCache implementation used to cache tile images. The default is null. /// public static Caching.IImageCache Cache; static TileImageLoader() { DefaultCacheExpiration = TimeSpan.FromDays(1); MinimumCacheExpiration = TimeSpan.FromHours(1); } private readonly ConcurrentStack pendingTiles = new ConcurrentStack(); private int taskCount; public void LoadTiles(MapTileLayer tileLayer) { pendingTiles.Clear(); var tiles = tileLayer.Tiles.Where(t => t.Pending); if (tiles.Any()) { var tileSource = tileLayer.TileSource; var imageTileSource = tileSource as ImageTileSource; if (imageTileSource != null) { LoadTiles(tiles, imageTileSource); } else { pendingTiles.PushRange(tiles.Reverse().ToArray()); var sourceName = tileLayer.SourceName; var maxDownloads = tileLayer.MaxParallelDownloads; while (taskCount < Math.Min(pendingTiles.Count, maxDownloads)) { Interlocked.Increment(ref taskCount); Task.Run(async () => { await LoadPendingTiles(tileSource, sourceName); Interlocked.Decrement(ref taskCount); }); } } } } private void LoadTiles(IEnumerable tiles, ImageTileSource tileSource) { foreach (var tile in tiles) { tile.Pending = false; try { var image = tileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel); if (image != null) { tile.SetImage(image); } } catch (Exception ex) { Debug.WriteLine("{0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message); } } } private async Task LoadPendingTiles(TileSource tileSource, string sourceName) { Tile tile; while (pendingTiles.TryPop(out tile)) { tile.Pending = false; try { var uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel); if (uri != null) { if (!uri.IsAbsoluteUri) { await LoadImageFromFile(tile, uri.OriginalString); } else if (uri.Scheme == "file") { await LoadImageFromFile(tile, uri.LocalPath); } else if (Cache == null || sourceName == null) { await DownloadImage(tile, uri, null); } else { var extension = Path.GetExtension(uri.LocalPath); if (string.IsNullOrEmpty(extension) || extension == ".jpeg") { extension = ".jpg"; } var cacheKey = string.Format(@"{0}\{1}\{2}\{3}{4}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y, extension); var cacheItem = await Cache.GetAsync(cacheKey); var loaded = false; if (cacheItem == null || cacheItem.Expiration <= DateTime.UtcNow) { loaded = await DownloadImage(tile, uri, cacheKey); } if (!loaded && cacheItem != null && cacheItem.Buffer != null) { using (var stream = new InMemoryRandomAccessStream()) { await stream.WriteAsync(cacheItem.Buffer); await stream.FlushAsync(); stream.Seek(0); await LoadImageFromStream(tile, stream); } } } } } catch (Exception ex) { Debug.WriteLine("{0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message); } } } private async Task DownloadImage(Tile tile, Uri uri, string cacheKey) { try { using (var httpClient = new HttpClient(new HttpBaseProtocolFilter { AllowAutoRedirect = false })) using (var response = await httpClient.GetAsync(uri)) { if (response.IsSuccessStatusCode) { string tileInfo; if (!response.Headers.TryGetValue("X-VE-Tile-Info", out tileInfo) || tileInfo != "no-tile") // set by Bing Maps { await LoadImageFromHttpResponse(response, tile, cacheKey); } return true; } Debug.WriteLine("{0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); } } catch (Exception ex) { Debug.WriteLine("{0}: {1}", uri, ex.Message); } return false; } private async Task LoadImageFromHttpResponse(HttpResponseMessage response, Tile tile, string cacheKey) { using (var stream = new InMemoryRandomAccessStream()) { using (var content = response.Content) { await content.WriteToStreamAsync(stream); } await stream.FlushAsync(); stream.Seek(0); await LoadImageFromStream(tile, stream); if (cacheKey != null) { var buffer = new Windows.Storage.Streams.Buffer((uint)stream.Size); stream.Seek(0); await stream.ReadAsync(buffer, buffer.Capacity, InputStreamOptions.None); var expiration = DefaultCacheExpiration; if (response.Headers.CacheControl.MaxAge.HasValue) { expiration = response.Headers.CacheControl.MaxAge.Value; if (expiration < MinimumCacheExpiration) { expiration = MinimumCacheExpiration; } } await Cache.SetAsync(cacheKey, buffer, DateTime.UtcNow.Add(expiration)); } } } private async Task LoadImageFromFile(Tile tile, string path) { try { var file = await StorageFile.GetFileFromPathAsync(path); using (var stream = await file.OpenReadAsync()) { await LoadImageFromStream(tile, stream); } } catch (Exception ex) { Debug.WriteLine("{0}: {1}", path, ex.Message); } } private async Task LoadImageFromStream(Tile tile, IRandomAccessStream stream) { var tcs = new TaskCompletionSource(); await tile.Image.Dispatcher.RunAsync(CoreDispatcherPriority.Low, async () => { try { var image = new BitmapImage(); await image.SetSourceAsync(stream); tile.SetImage(image, true, false); tcs.SetResult(null); } catch (Exception ex) { tcs.SetException(ex); } }); await tcs.Task; } } }