diff --git a/MapControl/Avalonia/ImageLoader.Avalonia.cs b/MapControl/Avalonia/ImageLoader.Avalonia.cs index 27e71872..b6ca7b0c 100644 --- a/MapControl/Avalonia/ImageLoader.Avalonia.cs +++ b/MapControl/Avalonia/ImageLoader.Avalonia.cs @@ -4,6 +4,7 @@ using Avalonia.Media.Imaging; using Avalonia.Platform; using System; using System.IO; +using System.Threading; using System.Threading.Tasks; namespace MapControl @@ -38,17 +39,18 @@ namespace MapControl } } - internal static async Task LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress progress) + internal static async Task LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress progress, CancellationToken cancellationToken) { WriteableBitmap mergedBitmap = null; var p1 = 0d; var p2 = 0d; var images = await Task.WhenAll( - LoadImageAsync(uri1, new Progress(p => { p1 = p; progress.Report((p1 + p2) / 2d); })), - LoadImageAsync(uri2, new Progress(p => { p2 = p; progress.Report((p1 + p2) / 2d); }))); + LoadImageAsync(uri1, new Progress(p => { p1 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken), + LoadImageAsync(uri2, new Progress(p => { p2 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken)); - if (images.Length == 2 && + if (!cancellationToken.IsCancellationRequested && + images.Length == 2 && images[0] is Bitmap bitmap1 && images[1] is Bitmap bitmap2 && bitmap1.PixelSize.Height == bitmap2.PixelSize.Height && diff --git a/MapControl/Shared/BoundingBoxTileSource.cs b/MapControl/Shared/BoundingBoxTileSource.cs index b28d7846..69bd2dc6 100644 --- a/MapControl/Shared/BoundingBoxTileSource.cs +++ b/MapControl/Shared/BoundingBoxTileSource.cs @@ -1,13 +1,5 @@ using System; using System.Globalization; -using System.Threading.Tasks; -#if WPF -using System.Windows.Media; -#elif UWP -using Windows.UI.Xaml.Media; -#elif WINUI -using Microsoft.UI.Xaml.Media; -#endif namespace MapControl { @@ -20,13 +12,6 @@ namespace MapControl return GetUri(west, south, east, north); } - public override Task LoadImageAsync(int column, int row, int zoomLevel) - { - GetTileBounds(column, row, zoomLevel, out double west, out double south, out double east, out double north); - - return LoadImageAsync(west, south, east, north); - } - protected virtual Uri GetUri(double west, double south, double east, double north) { Uri uri = null; @@ -46,13 +31,6 @@ namespace MapControl return uri; } - protected virtual Task LoadImageAsync(double west, double south, double east, double north) - { - var uri = GetUri(west, south, east, north); - - return uri != null ? ImageLoader.LoadImageAsync(uri) : Task.FromResult((ImageSource)null); - } - /// /// Gets the bounding box in meters of a standard Web Mercator tile, /// specified by grid column and row indices and zoom level. diff --git a/MapControl/Shared/GroundOverlay.cs b/MapControl/Shared/GroundOverlay.cs index e5c49480..5d7a39a9 100644 --- a/MapControl/Shared/GroundOverlay.cs +++ b/MapControl/Shared/GroundOverlay.cs @@ -7,6 +7,7 @@ using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; +using System.Threading; #if WPF using System.Windows; using System.Windows.Controls; @@ -164,7 +165,7 @@ namespace MapControl foreach (var imageOverlay in imageOverlays) { - imageOverlay.ImageSource = await ImageLoader.LoadImageAsync(new Uri(docUri, imageOverlay.ImagePath)); + imageOverlay.ImageSource = await ImageLoader.LoadImageAsync(new Uri(docUri, imageOverlay.ImagePath), null, CancellationToken.None); } return imageOverlays; diff --git a/MapControl/Shared/ImageLoader.cs b/MapControl/Shared/ImageLoader.cs index fa05efdb..c9b5445b 100644 --- a/MapControl/Shared/ImageLoader.cs +++ b/MapControl/Shared/ImageLoader.cs @@ -3,6 +3,8 @@ using System; using System.IO; using System.Net.Http; using System.Threading.Tasks; +using System.Threading; + #if WPF using System.Windows.Media; #elif UWP @@ -31,7 +33,7 @@ namespace MapControl HttpClient.DefaultRequestHeaders.Add("User-Agent", $"XAML-Map-Control/{typeof(ImageLoader).Assembly.GetName().Version}"); } - public static async Task LoadImageAsync(Uri uri, IProgress progress = null) + public static async Task LoadImageAsync(Uri uri, IProgress progress, CancellationToken cancellationToken) { ImageSource image = null; @@ -41,7 +43,7 @@ namespace MapControl { if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) { - var response = await GetHttpResponseAsync(uri, progress); + var response = await GetHttpResponseAsync(uri, progress, cancellationToken); if (response?.Buffer != null) { @@ -87,13 +89,13 @@ namespace MapControl } } - internal static async Task GetHttpResponseAsync(Uri uri, IProgress progress = null) + internal static async Task GetHttpResponseAsync(Uri uri, IProgress progress, CancellationToken cancellationToken) { HttpResponse response = null; try { - using (var responseMessage = await HttpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false)) + using (var responseMessage = await HttpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)) { if (responseMessage.IsSuccessStatusCode) { @@ -116,6 +118,10 @@ namespace MapControl } } } + catch (OperationCanceledException) + { + Logger?.LogTrace("Cancelled loading image from {uri}", uri); + } catch (Exception ex) { Logger?.LogError(ex, "Failed loading image from {uri}", uri); diff --git a/MapControl/Shared/MapImageLayer.cs b/MapControl/Shared/MapImageLayer.cs index 5b7454a9..2e246038 100644 --- a/MapControl/Shared/MapImageLayer.cs +++ b/MapControl/Shared/MapImageLayer.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; #if WPF using System.Windows; @@ -53,7 +54,7 @@ namespace MapControl private readonly Progress loadingProgress; private readonly DispatcherTimer updateTimer; - private bool updateInProgress; + private CancellationTokenSource cancellationTokenSource; public MapImageLayer() { @@ -165,39 +166,37 @@ namespace MapControl } } - protected abstract Task GetImageAsync(BoundingBox boundingBox, IProgress progress); + protected abstract Task GetImageAsync(BoundingBox boundingBox, IProgress progress, CancellationToken cancellationToken); protected async Task UpdateImageAsync() { - if (updateInProgress) + updateTimer.Stop(); + + if (cancellationTokenSource != null) { - // Update image on next tick, start timer if not running. - // - updateTimer.Run(); + cancellationTokenSource.Cancel(); + cancellationTokenSource = null; } - else + + if (ParentMap != null && ParentMap.ActualWidth > 0d && ParentMap.ActualHeight > 0d) { - updateInProgress = true; - updateTimer.Stop(); + var width = ParentMap.ActualWidth * RelativeImageSize; + var height = ParentMap.ActualHeight * RelativeImageSize; + var x = (ParentMap.ActualWidth - width) / 2d; + var y = (ParentMap.ActualHeight - height) / 2d; - ImageSource image = null; - BoundingBox boundingBox = null; + var boundingBox = ParentMap.ViewRectToBoundingBox(new Rect(x, y, width, height)); - if (ParentMap != null && ParentMap.ActualWidth > 0d && ParentMap.ActualHeight > 0d) + cancellationTokenSource = new CancellationTokenSource(); + + var image = await GetImageAsync(boundingBox, loadingProgress, cancellationTokenSource.Token); + + cancellationTokenSource = null; + + if (image != null) { - var width = ParentMap.ActualWidth * RelativeImageSize; - var height = ParentMap.ActualHeight * RelativeImageSize; - var x = (ParentMap.ActualWidth - width) / 2d; - var y = (ParentMap.ActualHeight - height) / 2d; - - boundingBox = ParentMap.ViewRectToBoundingBox(new Rect(x, y, width, height)); - - image = await GetImageAsync(boundingBox, loadingProgress); + SwapImages(image, boundingBox); } - - SwapImages(image, boundingBox); - - updateInProgress = false; } } diff --git a/MapControl/Shared/MapTileLayer.cs b/MapControl/Shared/MapTileLayer.cs index 9b2018f6..79c832fc 100644 --- a/MapControl/Shared/MapTileLayer.cs +++ b/MapControl/Shared/MapTileLayer.cs @@ -119,7 +119,7 @@ namespace MapControl TileMatrix = null; Children.Clear(); - await LoadTilesAsync(null, null); // stop TileImageLoader + CancelLoadTilesAsync(); } else if (SetTileMatrix() || resetTiles) { diff --git a/MapControl/Shared/MapTileLayerBase.cs b/MapControl/Shared/MapTileLayerBase.cs index d390e302..4681579f 100644 --- a/MapControl/Shared/MapTileLayerBase.cs +++ b/MapControl/Shared/MapTileLayerBase.cs @@ -196,6 +196,13 @@ namespace MapControl return TileImageLoader.LoadTilesAsync(tiles, TileSource, cacheName, loadingProgress); } + protected void CancelLoadTilesAsync() + { + TileImageLoader.CancelLoadTiles(); + + ClearValue(LoadingProgressProperty); + } + protected abstract void SetRenderTransform(); protected abstract Task UpdateTileLayerAsync(bool resetTiles); diff --git a/MapControl/Shared/TileImageLoader.cs b/MapControl/Shared/TileImageLoader.cs index 15e0ac93..cf4cbae5 100644 --- a/MapControl/Shared/TileImageLoader.cs +++ b/MapControl/Shared/TileImageLoader.cs @@ -8,6 +8,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using System.Threading; + #if WPF using System.Windows.Media; #elif UWP @@ -24,6 +26,8 @@ namespace MapControl public interface ITileImageLoader { Task LoadTilesAsync(IEnumerable tiles, TileSource tileSource, string cacheName, IProgress progress); + + void CancelLoadTiles(); } public partial class TileImageLoader : ITileImageLoader @@ -71,17 +75,26 @@ namespace MapControl private ConcurrentStack pendingTiles = new ConcurrentStack(); + private CancellationTokenSource cancellationTokenSource; + + public void CancelLoadTiles() + { + pendingTiles.Clear(); + + if (cancellationTokenSource != null) + { + cancellationTokenSource.Cancel(); + cancellationTokenSource = null; + } + } + /// /// Loads all pending tiles from the tiles collection. Tile image caching is enabled when the Cache - /// property is non-null and tileSource.UriFormat starts with "http" and cacheName is a non-empty string. + /// property is not null and tileSource.UriFormat starts with "http" and cacheName is a non-empty string. /// public async Task LoadTilesAsync(IEnumerable tiles, TileSource tileSource, string cacheName, IProgress progress) { - if (!pendingTiles.IsEmpty) - { - pendingTiles.Clear(); - progress?.Report(1d); - } + CancelLoadTiles(); if (tileSource != null && tiles != null && (tiles = tiles.Where(tile => tile.IsPending)).Any()) { @@ -102,22 +115,24 @@ namespace MapControl var tasks = new Task[taskCount]; var tileStack = pendingTiles; // pendingTiles member may change while tasks are running + cancellationTokenSource = new CancellationTokenSource(); + async Task LoadTilesFromQueueAsync() { while (tileStack.TryPop(out var tile)) // use captured tileStack variable in local function { tile.IsPending = false; + progress?.Report((double)(tileCount - tileStack.Count) / tileCount); + try { - await LoadTileImage(tile, tileSource, cacheName).ConfigureAwait(false); + await LoadTileImage(tile, tileSource, cacheName, cancellationTokenSource.Token).ConfigureAwait(false); } catch (Exception ex) { Logger?.LogError(ex, "Failed loading tile image {zoom}/{column}/{row}", tile.ZoomLevel, tile.Column, tile.Row); } - - progress?.Report((double)(tileCount - tileStack.Count) / tileCount); } } @@ -131,14 +146,14 @@ namespace MapControl } } - private static async Task LoadTileImage(Tile tile, TileSource tileSource, string cacheName) + private static async Task LoadTileImage(Tile tile, TileSource tileSource, string cacheName, CancellationToken cancellationToken) { // Pass tileSource.LoadImageAsync calls to platform-specific method // LoadTileImage(Tile, Func>) for execution on the UI thread in WinUI and UWP. if (string.IsNullOrEmpty(cacheName)) { - Task LoadImage() => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel); + Task LoadImage() => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel, cancellationToken); await LoadTileImage(tile, LoadImage).ConfigureAwait(false); } @@ -148,7 +163,7 @@ namespace MapControl if (uri != null) { - var buffer = await LoadCachedBuffer(tile, uri, cacheName).ConfigureAwait(false); + var buffer = await LoadCachedBuffer(tile, uri, cacheName, cancellationToken).ConfigureAwait(false); if (buffer != null && buffer.Length > 0) { @@ -160,7 +175,7 @@ namespace MapControl } } - private static async Task LoadCachedBuffer(Tile tile, Uri uri, string cacheName) + private static async Task LoadCachedBuffer(Tile tile, Uri uri, string cacheName, CancellationToken cancellationToken) { byte[] buffer = null; @@ -175,7 +190,7 @@ namespace MapControl try { - buffer = await Cache.GetAsync(cacheKey).ConfigureAwait(false); + buffer = await Cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -184,7 +199,7 @@ namespace MapControl if (buffer == null) { - var response = await ImageLoader.GetHttpResponseAsync(uri).ConfigureAwait(false); + var response = await ImageLoader.GetHttpResponseAsync(uri, null, cancellationToken).ConfigureAwait(false); if (response != null) { @@ -201,7 +216,7 @@ namespace MapControl : response.MaxAge.Value }; - await Cache.SetAsync(cacheKey, buffer, options).ConfigureAwait(false); + await Cache.SetAsync(cacheKey, buffer, options, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { diff --git a/MapControl/Shared/TileSource.cs b/MapControl/Shared/TileSource.cs index 1f142044..d656470d 100644 --- a/MapControl/Shared/TileSource.cs +++ b/MapControl/Shared/TileSource.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; #if WPF using System.Windows.Media; @@ -73,11 +74,11 @@ namespace MapControl /// Loads a tile ImageSource asynchronously from GetUri(column, row, zoomLevel). /// This method is called by TileImageLoader when caching is disabled. /// - public virtual Task LoadImageAsync(int column, int row, int zoomLevel) + public virtual Task LoadImageAsync(int column, int row, int zoomLevel, CancellationToken cancellationToken) { var uri = GetUri(column, row, zoomLevel); - return uri != null ? ImageLoader.LoadImageAsync(uri) : Task.FromResult((ImageSource)null); + return uri != null ? ImageLoader.LoadImageAsync(uri, null, cancellationToken) : Task.FromResult((ImageSource)null); } /// diff --git a/MapControl/Shared/WmsImageLayer.cs b/MapControl/Shared/WmsImageLayer.cs index fe58de6c..092c289e 100644 --- a/MapControl/Shared/WmsImageLayer.cs +++ b/MapControl/Shared/WmsImageLayer.cs @@ -5,6 +5,8 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; +using System.Threading; + #if WPF using System.Windows; using System.Windows.Media; @@ -162,7 +164,7 @@ namespace MapControl /// /// Loads an ImageSource from the URL returned by GetMapRequestUri(). /// - protected override async Task GetImageAsync(BoundingBox boundingBox, IProgress progress) + protected override async Task GetImageAsync(BoundingBox boundingBox, IProgress progress, CancellationToken cancellationToken) { ImageSource image = null; @@ -185,7 +187,7 @@ namespace MapControl if (uri != null) { - image = await ImageLoader.LoadImageAsync(new Uri(uri), progress); + image = await ImageLoader.LoadImageAsync(new Uri(uri), progress, cancellationToken); } } else @@ -208,7 +210,7 @@ namespace MapControl if (uri1 != null && uri2 != null) { - image = await ImageLoader.LoadMergedImageAsync(new Uri(uri1), new Uri(uri2), progress); + image = await ImageLoader.LoadMergedImageAsync(new Uri(uri1), new Uri(uri2), progress, cancellationToken); } } } diff --git a/MapControl/Shared/WmtsTileLayer.cs b/MapControl/Shared/WmtsTileLayer.cs index 3c23f9e2..5a2cc526 100644 --- a/MapControl/Shared/WmtsTileLayer.cs +++ b/MapControl/Shared/WmtsTileLayer.cs @@ -101,7 +101,7 @@ namespace MapControl { Children.Clear(); - await LoadTilesAsync(null, null); // stop TileImageLoader + CancelLoadTilesAsync(); } else if (UpdateChildLayers(tileMatrixSet)) { diff --git a/MapControl/WPF/ImageLoader.WPF.cs b/MapControl/WPF/ImageLoader.WPF.cs index 3b56b766..9371d50a 100644 --- a/MapControl/WPF/ImageLoader.WPF.cs +++ b/MapControl/WPF/ImageLoader.WPF.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; @@ -45,17 +46,18 @@ namespace MapControl } } - internal static async Task LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress progress) + internal static async Task LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress progress, CancellationToken cancellationToken) { WriteableBitmap mergedBitmap = null; var p1 = 0d; var p2 = 0d; var images = await Task.WhenAll( - LoadImageAsync(uri1, new Progress(p => { p1 = p; progress.Report((p1 + p2) / 2d); })), - LoadImageAsync(uri2, new Progress(p => { p2 = p; progress.Report((p1 + p2) / 2d); }))); + LoadImageAsync(uri1, new Progress(p => { p1 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken), + LoadImageAsync(uri2, new Progress(p => { p2 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken)); - if (images.Length == 2 && + if (!cancellationToken.IsCancellationRequested && + images.Length == 2 && images[0] is BitmapSource bitmap1 && images[1] is BitmapSource bitmap2 && bitmap1.PixelHeight == bitmap2.PixelHeight && diff --git a/MapControl/WinUI/ImageLoader.WinUI.cs b/MapControl/WinUI/ImageLoader.WinUI.cs index 848e3f0c..f4976b32 100644 --- a/MapControl/WinUI/ImageLoader.WinUI.cs +++ b/MapControl/WinUI/ImageLoader.WinUI.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading; using System.Threading.Tasks; using Windows.Graphics.Imaging; using Windows.Storage; @@ -66,7 +67,7 @@ namespace MapControl return image; } - internal static async Task LoadWriteableBitmapAsync(Uri uri, IProgress progress) + internal static async Task LoadWriteableBitmapAsync(Uri uri, IProgress progress, CancellationToken cancellationToken) { WriteableBitmap bitmap = null; @@ -74,7 +75,7 @@ namespace MapControl try { - var response = await GetHttpResponseAsync(uri, progress); + var response = await GetHttpResponseAsync(uri, progress, cancellationToken); if (response?.Buffer != null) { @@ -97,17 +98,18 @@ namespace MapControl return bitmap; } - internal static async Task LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress progress) + internal static async Task LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress progress, CancellationToken cancellationToken) { WriteableBitmap mergedBitmap = null; var p1 = 0d; var p2 = 0d; var bitmaps = await Task.WhenAll( - LoadWriteableBitmapAsync(uri1, new Progress(p => { p1 = p; progress.Report((p1 + p2) / 2d); })), - LoadWriteableBitmapAsync(uri2, new Progress(p => { p2 = p; progress.Report((p1 + p2) / 2d); }))); + LoadWriteableBitmapAsync(uri1, new Progress(p => { p1 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken), + LoadWriteableBitmapAsync(uri2, new Progress(p => { p2 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken)); - if (bitmaps.Length == 2 && + if (!cancellationToken.IsCancellationRequested && + bitmaps.Length == 2 && bitmaps[0] != null && bitmaps[1] != null && bitmaps[0].PixelHeight == bitmaps[1].PixelHeight)