diff --git a/MapControl/Shared/ImageLoader.cs b/MapControl/Shared/ImageLoader.cs index e9242177..7aadf7bd 100644 --- a/MapControl/Shared/ImageLoader.cs +++ b/MapControl/Shared/ImageLoader.cs @@ -3,14 +3,17 @@ // Licensed under the Microsoft Public License (Ms-PL) using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; #if WINDOWS_UWP -using Windows.Web.Http; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Imaging; #else -using System.Net.Http; using System.Windows.Media; using System.Windows.Media.Imaging; #endif @@ -36,7 +39,26 @@ namespace MapControl } else if (uri.Scheme == "http" || uri.Scheme == "https") { - image = await LoadHttpImageAsync(uri); + using (var response = await HttpClient.GetAsync(uri)) + { + if (response.IsSuccessStatusCode) + { + if (ImageAvailable(response.Headers)) + { + using (var stream = new MemoryStream()) + { + await response.Content.CopyToAsync(stream); + stream.Seek(0, SeekOrigin.Begin); + + image = await LoadImageAsync(stream); + } + } + } + else + { + Debug.WriteLine("ImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); + } + } } else { @@ -51,23 +73,50 @@ namespace MapControl return image; } - private static async Task LoadHttpImageAsync(Uri uri) + internal class ImageStream : MemoryStream { - ImageSource image = null; + public TimeSpan? MaxAge { get; set; } + } - using (var response = await HttpClient.GetAsync(uri)) + internal static async Task LoadImageStreamAsync(Uri uri) + { + ImageStream stream = null; + + try { - if (!response.IsSuccessStatusCode) + using (var response = await HttpClient.GetAsync(uri).ConfigureAwait(false)) { - Debug.WriteLine("ImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); - } - else if (ImageAvailable(response.Headers)) - { - image = await LoadImageAsync(response.Content); + if (response.IsSuccessStatusCode) + { + stream = new ImageStream(); + + if (ImageAvailable(response.Headers)) + { + await response.Content.CopyToAsync(stream).ConfigureAwait(false); + stream.Seek(0, SeekOrigin.Begin); + + stream.MaxAge = response.Headers.CacheControl?.MaxAge; + } + } + else + { + Debug.WriteLine("ImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); + } } } + catch (Exception ex) + { + Debug.WriteLine("ImageLoader: {0}: {1}", uri, ex.Message); + } - return image; + return stream; + } + + private static bool ImageAvailable(HttpResponseHeaders responseHeaders) + { + IEnumerable tileInfo; + + return !responseHeaders.TryGetValues("X-VE-Tile-Info", out tileInfo) || !tileInfo.Contains("no-tile"); } } } \ No newline at end of file diff --git a/MapControl/Shared/MapTileLayer.cs b/MapControl/Shared/MapTileLayer.cs index bd3df194..aa9d52df 100644 --- a/MapControl/Shared/MapTileLayer.cs +++ b/MapControl/Shared/MapTileLayer.cs @@ -66,6 +66,9 @@ namespace MapControl public static readonly DependencyProperty MaxZoomLevelProperty = DependencyProperty.Register( nameof(MaxZoomLevel), typeof(int), typeof(MapTileLayer), new PropertyMetadata(18)); + public static readonly DependencyProperty MaxBackgroundZoomLevelsProperty = DependencyProperty.Register( + nameof(MaxBackgroundZoomLevels), typeof(int), typeof(MapTileLayer), new PropertyMetadata(8)); + public static readonly DependencyProperty UpdateIntervalProperty = DependencyProperty.Register( nameof(UpdateInterval), typeof(TimeSpan), typeof(MapTileLayer), new PropertyMetadata(TimeSpan.FromSeconds(0.2), (o, e) => ((MapTileLayer)o).updateTimer.Interval = (TimeSpan)e.NewValue)); @@ -142,7 +145,7 @@ namespace MapControl } /// - /// Minimum zoom level supported by the MapTileLayer. + /// Minimum zoom level supported by the MapTileLayer. Default value is 0. /// public int MinZoomLevel { @@ -151,7 +154,7 @@ namespace MapControl } /// - /// Maximum zoom level supported by the MapTileLayer. + /// Maximum zoom level supported by the MapTileLayer. Default value is 18. /// public int MaxZoomLevel { @@ -159,6 +162,16 @@ namespace MapControl set { SetValue(MaxZoomLevelProperty, value); } } + /// + /// Maximum number of background tile levels. Default value is 8. + /// Applies only to a MapTileLayer that is the MapLayer of its ParentMap. + /// + public int MaxBackgroundZoomLevels + { + get { return (int)GetValue(MaxBackgroundZoomLevelsProperty); } + set { SetValue(MaxBackgroundZoomLevelsProperty, value); } + } + /// /// Minimum time interval between tile updates. /// @@ -342,41 +355,45 @@ namespace MapControl if (parentMap != null && TileGrid != null && TileSource != null) { var maxZoomLevel = Math.Min(TileGrid.ZoomLevel, MaxZoomLevel); - var minZoomLevel = MinZoomLevel; - if (minZoomLevel < maxZoomLevel && parentMap.MapLayer != this) + if (maxZoomLevel >= MinZoomLevel) { - minZoomLevel = maxZoomLevel; // do not load lower level tiles if this is note a "base" layer - } + var minZoomLevel = maxZoomLevel; - for (var z = minZoomLevel; z <= maxZoomLevel; z++) - { - var tileSize = 1 << (TileGrid.ZoomLevel - z); - var x1 = (int)Math.Floor((double)TileGrid.XMin / tileSize); // may be negative - var x2 = TileGrid.XMax / tileSize; - var y1 = Math.Max(TileGrid.YMin / tileSize, 0); - var y2 = Math.Min(TileGrid.YMax / tileSize, (1 << z) - 1); - - for (var y = y1; y <= y2; y++) + if (this == parentMap.MapLayer) // load background tiles { - for (var x = x1; x <= x2; x++) + minZoomLevel = Math.Max(TileGrid.ZoomLevel - MaxBackgroundZoomLevels, MinZoomLevel); + } + + for (var z = minZoomLevel; z <= maxZoomLevel; z++) + { + var tileSize = 1 << (TileGrid.ZoomLevel - z); + var x1 = (int)Math.Floor((double)TileGrid.XMin / tileSize); // may be negative + var x2 = TileGrid.XMax / tileSize; + var y1 = Math.Max(TileGrid.YMin / tileSize, 0); + var y2 = Math.Min(TileGrid.YMax / tileSize, (1 << z) - 1); + + for (var y = y1; y <= y2; y++) { - var tile = Tiles.FirstOrDefault(t => t.ZoomLevel == z && t.X == x && t.Y == y); - - if (tile == null) + for (var x = x1; x <= x2; x++) { - tile = new Tile(z, x, y); + var tile = Tiles.FirstOrDefault(t => t.ZoomLevel == z && t.X == x && t.Y == y); - var equivalentTile = Tiles.FirstOrDefault( - t => t.ZoomLevel == z && t.XIndex == tile.XIndex && t.Y == y && t.Image.Source != null); - - if (equivalentTile != null) + if (tile == null) { - tile.SetImage(equivalentTile.Image.Source, false); // no fade-in animation - } - } + tile = new Tile(z, x, y); - newTiles.Add(tile); + var equivalentTile = Tiles.FirstOrDefault( + t => t.ZoomLevel == z && t.XIndex == tile.XIndex && t.Y == y && t.Image.Source != null); + + if (equivalentTile != null) + { + tile.SetImage(equivalentTile.Image.Source, false); // no fade-in animation + } + } + + newTiles.Add(tile); + } } } } diff --git a/MapControl/Shared/TileImageLoader.cs b/MapControl/Shared/TileImageLoader.cs index fae6dd50..963d40da 100644 --- a/MapControl/Shared/TileImageLoader.cs +++ b/MapControl/Shared/TileImageLoader.cs @@ -3,9 +3,11 @@ // 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; @@ -45,13 +47,27 @@ namespace MapControl public static string CacheKeyFormat { get; set; } = "{0}/{1}/{2}/{3}{4}"; + public 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 in up to MaxLoadTasks parallel Tasks. + /// 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. + /// tile images will be cached in the TileImageLoader's Cache (if it's not null). /// public void LoadTilesAsync(IEnumerable tiles, TileSource tileSource, string sourceName) { @@ -59,31 +75,41 @@ namespace MapControl if (tileSource != null) { - tileQueue.Enqueue(tiles); + SetLoadTileImageFunction(tileSource, sourceName); - var newTasks = Math.Min(tileQueue.Count, MaxLoadTasks) - taskCount; + tiles = tiles.Where(tile => tile.Pending); - if (newTasks > 0) + if (tiles.Any()) { - Interlocked.Add(ref taskCount, newTasks); + tileQueue.Enqueue(tiles); - while (--newTasks >= 0) + var newTasks = Math.Min(tileQueue.Count, MaxLoadTasks) - taskCount; + + if (newTasks > 0) { - Task.Run(() => LoadTilesFromQueueAsync(tileSource, sourceName)); + var loadTasks = Enumerable.Range(0, newTasks).Select(n => LoadTilesFromQueueAsync()); + + Interlocked.Add(ref taskCount, newTasks); + + Task.WhenAll(loadTasks); // not awaited } } } } - private async Task LoadTilesFromQueueAsync(TileSource tileSource, string sourceName) + private async Task LoadTilesFromQueueAsync() { Tile tile; while (tileQueue.TryDequeue(out tile)) { + tile.Pending = false; + try { - await LoadTileImageAsync(tile, tileSource, sourceName).ConfigureAwait(false); + Debug.WriteLine("TileImageLoader: loading {0}/{1}/{2} in thread {3}", tile.ZoomLevel, tile.XIndex, tile.Y, Environment.CurrentManagedThreadId); + + await loadTileImage(tile).ConfigureAwait(false); } catch (Exception ex) { @@ -94,32 +120,37 @@ namespace MapControl Interlocked.Decrement(ref taskCount); } - private static async Task LoadTileImageAsync(Tile tile, TileSource tileSource, string sourceName) + private void SetLoadTileImageFunction(TileSource tileSource, string sourceName) { if (Cache != null && tileSource.UriFormat != null && tileSource.UriFormat.StartsWith("http") && !string.IsNullOrEmpty(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); - } + loadTileImage = tile => LoadTileImageAsync(tile, tileSource, sourceName); } else { - await LoadTileImageAsync(tile, tileSource).ConfigureAwait(false); + loadTileImage = tile => LoadTileImageAsync(tile, tileSource); + } + } + + private static async Task LoadTileImageAsync(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); } } diff --git a/MapControl/Shared/TileQueue.cs b/MapControl/Shared/TileQueue.cs deleted file mode 100644 index d3907901..00000000 --- a/MapControl/Shared/TileQueue.cs +++ /dev/null @@ -1,35 +0,0 @@ -// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control -// © 2019 Clemens Fischer -// Licensed under the Microsoft Public License (Ms-PL) - -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; - -namespace MapControl -{ - public class TileQueue : ConcurrentStack - { - public void Enqueue(IEnumerable tiles) - { - tiles = tiles.Where(tile => tile.Pending); - - if (tiles.Any()) - { - PushRange(tiles.Reverse().ToArray()); - } - } - - public bool TryDequeue(out Tile tile) - { - var success = TryPop(out tile); - - if (success) - { - tile.Pending = false; - } - - return success; - } - } -} diff --git a/MapControl/UWP/ImageLoader.UWP.cs b/MapControl/UWP/ImageLoader.UWP.cs index 1a3a862b..7f45dac7 100644 --- a/MapControl/UWP/ImageLoader.UWP.cs +++ b/MapControl/UWP/ImageLoader.UWP.cs @@ -3,7 +3,6 @@ // Licensed under the Microsoft Public License (Ms-PL) using System; -using System.Diagnostics; using System.IO; using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; @@ -11,13 +10,16 @@ using Windows.Storage; using Windows.Storage.Streams; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Imaging; -using Windows.Web.Http; -using Windows.Web.Http.Headers; namespace MapControl { public static partial class ImageLoader { + public static Task LoadImageAsync(Stream stream) + { + return LoadImageAsync(stream.AsRandomAccessStream()); + } + public static async Task LoadImageAsync(IRandomAccessStream stream) { var image = new BitmapImage(); @@ -54,68 +56,5 @@ namespace MapControl return image; } - - private static async Task LoadImageAsync(IHttpContent content) - { - using (var stream = new InMemoryRandomAccessStream()) - { - await content.WriteToStreamAsync(stream); - stream.Seek(0); - - return await LoadImageAsync(stream); - } - } - - internal class HttpBufferResponse - { - public readonly IBuffer Buffer; - public readonly TimeSpan? MaxAge; - - public HttpBufferResponse(IBuffer buffer, TimeSpan? maxAge) - { - Buffer = buffer; - MaxAge = maxAge; - } - } - - internal static async Task LoadHttpBufferAsync(Uri uri) - { - HttpBufferResponse response = null; - - try - { - using (var responseMessage = await HttpClient.GetAsync(uri)) - { - if (responseMessage.IsSuccessStatusCode) - { - IBuffer buffer = null; - TimeSpan? maxAge = null; - - if (ImageAvailable(responseMessage.Headers)) - { - buffer = await responseMessage.Content.ReadAsBufferAsync(); - maxAge = responseMessage.Headers.CacheControl?.MaxAge; - } - - response = new HttpBufferResponse(buffer, maxAge); - } - else - { - Debug.WriteLine("ImageLoader: {0}: {1} {2}", uri, (int)responseMessage.StatusCode, responseMessage.ReasonPhrase); - } - } - } - catch (Exception ex) - { - Debug.WriteLine("ImageLoader: {0}: {1}", uri, ex.Message); - } - - return response; - } - - private static bool ImageAvailable(HttpResponseHeaderCollection responseHeaders) - { - return !responseHeaders.TryGetValue("X-VE-Tile-Info", out string tileInfo) || tileInfo != "no-tile"; - } } } diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index 5768a8db..175e8f07 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -139,9 +139,6 @@ TileImageLoader.cs - - TileQueue.cs - TileSource.cs diff --git a/MapControl/UWP/TileImageLoader.UWP.cs b/MapControl/UWP/TileImageLoader.UWP.cs index 5aaf066d..12fb60d7 100644 --- a/MapControl/UWP/TileImageLoader.UWP.cs +++ b/MapControl/UWP/TileImageLoader.UWP.cs @@ -3,6 +3,7 @@ // Licensed under the Microsoft Public License (Ms-PL) using System; +using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.Streams; @@ -35,34 +36,31 @@ namespace MapControl if (cacheBuffer == null || cacheItem.Expiration < DateTime.UtcNow) { - var response = await ImageLoader.LoadHttpBufferAsync(uri).ConfigureAwait(false); - - if (response != null) // download succeeded + using (var stream = await ImageLoader.LoadImageStreamAsync(uri).ConfigureAwait(false)) { - cacheBuffer = null; // discard cached image - - if (response.Buffer != null) // tile image available + if (stream != null) // download succeeded { - await LoadTileImageAsync(tile, response.Buffer).ConfigureAwait(false); - await Cache.SetAsync(cacheKey, response.Buffer, GetExpiration(response.MaxAge)).ConfigureAwait(false); + cacheBuffer = null; // discard cached image + + if (stream.Length > 0) // tile image available + { + await SetTileImageAsync(tile, () => ImageLoader.LoadImageAsync(stream)).ConfigureAwait(false); + + await Cache.SetAsync(cacheKey, stream.ToArray().AsBuffer(), GetExpiration(stream.MaxAge)).ConfigureAwait(false); + } } } } if (cacheBuffer != null) // cached image not expired or download failed { - await LoadTileImageAsync(tile, cacheBuffer).ConfigureAwait(false); - } - } + using (var stream = new InMemoryRandomAccessStream()) + { + await stream.WriteAsync(cacheBuffer); + stream.Seek(0); - private static async Task LoadTileImageAsync(Tile tile, IBuffer buffer) - { - using (var stream = new InMemoryRandomAccessStream()) - { - await stream.WriteAsync(buffer); - stream.Seek(0); - - await SetTileImageAsync(tile, () => ImageLoader.LoadImageAsync(stream)).ConfigureAwait(false); + await SetTileImageAsync(tile, () => ImageLoader.LoadImageAsync(stream)).ConfigureAwait(false); + } } } diff --git a/MapControl/WPF/ImageLoader.WPF.cs b/MapControl/WPF/ImageLoader.WPF.cs index 28573194..2fb479f1 100644 --- a/MapControl/WPF/ImageLoader.WPF.cs +++ b/MapControl/WPF/ImageLoader.WPF.cs @@ -2,13 +2,7 @@ // © 2019 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) -using System; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -67,72 +61,5 @@ namespace MapControl { return Task.Run(() => LoadImage(path)); } - - private static async Task LoadImageAsync(HttpContent content) - { - using (var stream = new MemoryStream()) - { - await content.CopyToAsync(stream); - stream.Seek(0, SeekOrigin.Begin); - - return LoadImage(stream); - } - } - - internal class HttpStreamResponse - { - public readonly MemoryStream Stream; - public readonly TimeSpan? MaxAge; - - public HttpStreamResponse(MemoryStream stream, TimeSpan? maxAge) - { - Stream = stream; - MaxAge = maxAge; - } - } - - internal static async Task LoadHttpStreamAsync(Uri uri) - { - HttpStreamResponse response = null; - - try - { - using (var responseMessage = await HttpClient.GetAsync(uri).ConfigureAwait(false)) - { - if (responseMessage.IsSuccessStatusCode) - { - MemoryStream stream = null; - TimeSpan? maxAge = null; - - if (ImageAvailable(responseMessage.Headers)) - { - stream = new MemoryStream(); - await responseMessage.Content.CopyToAsync(stream).ConfigureAwait(false); - stream.Seek(0, SeekOrigin.Begin); - - maxAge = responseMessage.Headers.CacheControl?.MaxAge; - } - - response = new HttpStreamResponse(stream, maxAge); - } - else - { - Debug.WriteLine("ImageLoader: {0}: {1} {2}", uri, (int)responseMessage.StatusCode, responseMessage.ReasonPhrase); - } - } - } - catch (Exception ex) - { - Debug.WriteLine("ImageLoader: {0}: {1}", uri, ex.Message); - } - - return response; - } - - private static bool ImageAvailable(HttpResponseHeaders responseHeaders) - { - IEnumerable tileInfo; - return !responseHeaders.TryGetValues("X-VE-Tile-Info", out tileInfo) || !tileInfo.Contains("no-tile"); - } } } diff --git a/MapControl/WPF/MapControl.WPF.csproj b/MapControl/WPF/MapControl.WPF.csproj index 1f21b50b..52b37100 100644 --- a/MapControl/WPF/MapControl.WPF.csproj +++ b/MapControl/WPF/MapControl.WPF.csproj @@ -164,9 +164,6 @@ TileImageLoader.cs - - TileQueue.cs - TileSource.cs diff --git a/MapControl/WPF/TileImageLoader.WPF.cs b/MapControl/WPF/TileImageLoader.WPF.cs index 154cfda5..efb19097 100644 --- a/MapControl/WPF/TileImageLoader.WPF.cs +++ b/MapControl/WPF/TileImageLoader.WPF.cs @@ -32,22 +32,23 @@ namespace MapControl { ImageSource image = null; DateTime expiration; - var cacheBuffer = GetCachedImage(cacheKey, out expiration); + byte[] cacheBuffer; + + GetCachedImage(cacheKey, out cacheBuffer, out expiration); if (cacheBuffer == null || expiration < DateTime.UtcNow) { - var response = await ImageLoader.LoadHttpStreamAsync(uri).ConfigureAwait(false); - - if (response != null) // download succeeded + using (var stream = await ImageLoader.LoadImageStreamAsync(uri).ConfigureAwait(false)) { - cacheBuffer = null; // discard cached image - - if (response.Stream != null) // tile image available + if (stream != null) // download succeeded { - using (var stream = response.Stream) + cacheBuffer = null; // discard cached image + + if (stream.Length > 0) // tile image available { image = ImageLoader.LoadImage(stream); - SetCachedImage(cacheKey, stream, GetExpiration(response.MaxAge)); + + SetCachedImage(cacheKey, stream, GetExpiration(stream.MaxAge)); } } } @@ -79,9 +80,9 @@ namespace MapControl tile.Image.Dispatcher.InvokeAsync(() => tile.SetImage(image)); } - private static byte[] GetCachedImage(string cacheKey, out DateTime expiration) + private static void GetCachedImage(string cacheKey, out byte[] buffer, out DateTime expiration) { - var buffer = Cache.Get(cacheKey) as byte[]; + buffer = Cache.Get(cacheKey) as byte[]; if (buffer != null && buffer.Length >= 16 && Encoding.ASCII.GetString(buffer, buffer.Length - 16, 8) == "EXPIRES:") @@ -92,8 +93,6 @@ namespace MapControl { expiration = DateTime.MinValue; } - - return buffer; } private static void SetCachedImage(string cacheKey, MemoryStream stream, DateTime expiration)