Image loading with cancellation

This commit is contained in:
ClemensFischer 2025-08-19 23:20:11 +02:00
parent 81eabef257
commit 69bba213f0
13 changed files with 103 additions and 88 deletions

View file

@ -4,6 +4,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
using System; using System;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MapControl namespace MapControl
@ -38,17 +39,18 @@ namespace MapControl
} }
} }
internal static async Task<IImage> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress) internal static async Task<IImage> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress, CancellationToken cancellationToken)
{ {
WriteableBitmap mergedBitmap = null; WriteableBitmap mergedBitmap = null;
var p1 = 0d; var p1 = 0d;
var p2 = 0d; var p2 = 0d;
var images = await Task.WhenAll( var images = await Task.WhenAll(
LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })), LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken),
LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); }))); LoadImageAsync(uri2, new Progress<double>(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[0] is Bitmap bitmap1 &&
images[1] is Bitmap bitmap2 && images[1] is Bitmap bitmap2 &&
bitmap1.PixelSize.Height == bitmap2.PixelSize.Height && bitmap1.PixelSize.Height == bitmap2.PixelSize.Height &&

View file

@ -1,13 +1,5 @@
using System; using System;
using System.Globalization; 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 namespace MapControl
{ {
@ -20,13 +12,6 @@ namespace MapControl
return GetUri(west, south, east, north); return GetUri(west, south, east, north);
} }
public override Task<ImageSource> 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) protected virtual Uri GetUri(double west, double south, double east, double north)
{ {
Uri uri = null; Uri uri = null;
@ -46,13 +31,6 @@ namespace MapControl
return uri; return uri;
} }
protected virtual Task<ImageSource> 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);
}
/// <summary> /// <summary>
/// Gets the bounding box in meters of a standard Web Mercator tile, /// Gets the bounding box in meters of a standard Web Mercator tile,
/// specified by grid column and row indices and zoom level. /// specified by grid column and row indices and zoom level.

View file

@ -7,6 +7,7 @@ using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
using System.Threading;
#if WPF #if WPF
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
@ -164,7 +165,7 @@ namespace MapControl
foreach (var imageOverlay in imageOverlays) 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; return imageOverlays;

View file

@ -3,6 +3,8 @@ using System;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading;
#if WPF #if WPF
using System.Windows.Media; using System.Windows.Media;
#elif UWP #elif UWP
@ -31,7 +33,7 @@ namespace MapControl
HttpClient.DefaultRequestHeaders.Add("User-Agent", $"XAML-Map-Control/{typeof(ImageLoader).Assembly.GetName().Version}"); HttpClient.DefaultRequestHeaders.Add("User-Agent", $"XAML-Map-Control/{typeof(ImageLoader).Assembly.GetName().Version}");
} }
public static async Task<ImageSource> LoadImageAsync(Uri uri, IProgress<double> progress = null) public static async Task<ImageSource> LoadImageAsync(Uri uri, IProgress<double> progress, CancellationToken cancellationToken)
{ {
ImageSource image = null; ImageSource image = null;
@ -41,7 +43,7 @@ namespace MapControl
{ {
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) 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) if (response?.Buffer != null)
{ {
@ -87,13 +89,13 @@ namespace MapControl
} }
} }
internal static async Task<HttpResponse> GetHttpResponseAsync(Uri uri, IProgress<double> progress = null) internal static async Task<HttpResponse> GetHttpResponseAsync(Uri uri, IProgress<double> progress, CancellationToken cancellationToken)
{ {
HttpResponse response = null; HttpResponse response = null;
try 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) if (responseMessage.IsSuccessStatusCode)
{ {
@ -116,6 +118,10 @@ namespace MapControl
} }
} }
} }
catch (OperationCanceledException)
{
Logger?.LogTrace("Cancelled loading image from {uri}", uri);
}
catch (Exception ex) catch (Exception ex)
{ {
Logger?.LogError(ex, "Failed loading image from {uri}", uri); Logger?.LogError(ex, "Failed loading image from {uri}", uri);

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
#if WPF #if WPF
using System.Windows; using System.Windows;
@ -53,7 +54,7 @@ namespace MapControl
private readonly Progress<double> loadingProgress; private readonly Progress<double> loadingProgress;
private readonly DispatcherTimer updateTimer; private readonly DispatcherTimer updateTimer;
private bool updateInProgress; private CancellationTokenSource cancellationTokenSource;
public MapImageLayer() public MapImageLayer()
{ {
@ -165,23 +166,17 @@ namespace MapControl
} }
} }
protected abstract Task<ImageSource> GetImageAsync(BoundingBox boundingBox, IProgress<double> progress); protected abstract Task<ImageSource> GetImageAsync(BoundingBox boundingBox, IProgress<double> progress, CancellationToken cancellationToken);
protected async Task UpdateImageAsync() protected async Task UpdateImageAsync()
{ {
if (updateInProgress)
{
// Update image on next tick, start timer if not running.
//
updateTimer.Run();
}
else
{
updateInProgress = true;
updateTimer.Stop(); updateTimer.Stop();
ImageSource image = null; if (cancellationTokenSource != null)
BoundingBox boundingBox = null; {
cancellationTokenSource.Cancel();
cancellationTokenSource = null;
}
if (ParentMap != null && ParentMap.ActualWidth > 0d && ParentMap.ActualHeight > 0d) if (ParentMap != null && ParentMap.ActualWidth > 0d && ParentMap.ActualHeight > 0d)
{ {
@ -190,14 +185,18 @@ namespace MapControl
var x = (ParentMap.ActualWidth - width) / 2d; var x = (ParentMap.ActualWidth - width) / 2d;
var y = (ParentMap.ActualHeight - height) / 2d; var y = (ParentMap.ActualHeight - height) / 2d;
boundingBox = ParentMap.ViewRectToBoundingBox(new Rect(x, y, width, height)); var boundingBox = ParentMap.ViewRectToBoundingBox(new Rect(x, y, width, height));
image = await GetImageAsync(boundingBox, loadingProgress); cancellationTokenSource = new CancellationTokenSource();
}
var image = await GetImageAsync(boundingBox, loadingProgress, cancellationTokenSource.Token);
cancellationTokenSource = null;
if (image != null)
{
SwapImages(image, boundingBox); SwapImages(image, boundingBox);
}
updateInProgress = false;
} }
} }

View file

@ -119,7 +119,7 @@ namespace MapControl
TileMatrix = null; TileMatrix = null;
Children.Clear(); Children.Clear();
await LoadTilesAsync(null, null); // stop TileImageLoader CancelLoadTilesAsync();
} }
else if (SetTileMatrix() || resetTiles) else if (SetTileMatrix() || resetTiles)
{ {

View file

@ -196,6 +196,13 @@ namespace MapControl
return TileImageLoader.LoadTilesAsync(tiles, TileSource, cacheName, loadingProgress); return TileImageLoader.LoadTilesAsync(tiles, TileSource, cacheName, loadingProgress);
} }
protected void CancelLoadTilesAsync()
{
TileImageLoader.CancelLoadTiles();
ClearValue(LoadingProgressProperty);
}
protected abstract void SetRenderTransform(); protected abstract void SetRenderTransform();
protected abstract Task UpdateTileLayerAsync(bool resetTiles); protected abstract Task UpdateTileLayerAsync(bool resetTiles);

View file

@ -8,6 +8,8 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading;
#if WPF #if WPF
using System.Windows.Media; using System.Windows.Media;
#elif UWP #elif UWP
@ -24,6 +26,8 @@ namespace MapControl
public interface ITileImageLoader public interface ITileImageLoader
{ {
Task LoadTilesAsync(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress); Task LoadTilesAsync(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress);
void CancelLoadTiles();
} }
public partial class TileImageLoader : ITileImageLoader public partial class TileImageLoader : ITileImageLoader
@ -71,17 +75,26 @@ namespace MapControl
private ConcurrentStack<Tile> pendingTiles = new ConcurrentStack<Tile>(); private ConcurrentStack<Tile> pendingTiles = new ConcurrentStack<Tile>();
private CancellationTokenSource cancellationTokenSource;
public void CancelLoadTiles()
{
pendingTiles.Clear();
if (cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
cancellationTokenSource = null;
}
}
/// <summary> /// <summary>
/// Loads all pending tiles from the tiles collection. Tile image caching is enabled when the Cache /// 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.
/// </summary> /// </summary>
public async Task LoadTilesAsync(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress) public async Task LoadTilesAsync(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress)
{ {
if (!pendingTiles.IsEmpty) CancelLoadTiles();
{
pendingTiles.Clear();
progress?.Report(1d);
}
if (tileSource != null && tiles != null && (tiles = tiles.Where(tile => tile.IsPending)).Any()) if (tileSource != null && tiles != null && (tiles = tiles.Where(tile => tile.IsPending)).Any())
{ {
@ -102,22 +115,24 @@ namespace MapControl
var tasks = new Task[taskCount]; var tasks = new Task[taskCount];
var tileStack = pendingTiles; // pendingTiles member may change while tasks are running var tileStack = pendingTiles; // pendingTiles member may change while tasks are running
cancellationTokenSource = new CancellationTokenSource();
async Task LoadTilesFromQueueAsync() async Task LoadTilesFromQueueAsync()
{ {
while (tileStack.TryPop(out var tile)) // use captured tileStack variable in local function while (tileStack.TryPop(out var tile)) // use captured tileStack variable in local function
{ {
tile.IsPending = false; tile.IsPending = false;
progress?.Report((double)(tileCount - tileStack.Count) / tileCount);
try try
{ {
await LoadTileImage(tile, tileSource, cacheName).ConfigureAwait(false); await LoadTileImage(tile, tileSource, cacheName, cancellationTokenSource.Token).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger?.LogError(ex, "Failed loading tile image {zoom}/{column}/{row}", tile.ZoomLevel, tile.Column, tile.Row); 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 // Pass tileSource.LoadImageAsync calls to platform-specific method
// LoadTileImage(Tile, Func<Task<ImageSource>>) for execution on the UI thread in WinUI and UWP. // LoadTileImage(Tile, Func<Task<ImageSource>>) for execution on the UI thread in WinUI and UWP.
if (string.IsNullOrEmpty(cacheName)) if (string.IsNullOrEmpty(cacheName))
{ {
Task<ImageSource> LoadImage() => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel); Task<ImageSource> LoadImage() => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel, cancellationToken);
await LoadTileImage(tile, LoadImage).ConfigureAwait(false); await LoadTileImage(tile, LoadImage).ConfigureAwait(false);
} }
@ -148,7 +163,7 @@ namespace MapControl
if (uri != null) 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) if (buffer != null && buffer.Length > 0)
{ {
@ -160,7 +175,7 @@ namespace MapControl
} }
} }
private static async Task<byte[]> LoadCachedBuffer(Tile tile, Uri uri, string cacheName) private static async Task<byte[]> LoadCachedBuffer(Tile tile, Uri uri, string cacheName, CancellationToken cancellationToken)
{ {
byte[] buffer = null; byte[] buffer = null;
@ -175,7 +190,7 @@ namespace MapControl
try try
{ {
buffer = await Cache.GetAsync(cacheKey).ConfigureAwait(false); buffer = await Cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -184,7 +199,7 @@ namespace MapControl
if (buffer == null) if (buffer == null)
{ {
var response = await ImageLoader.GetHttpResponseAsync(uri).ConfigureAwait(false); var response = await ImageLoader.GetHttpResponseAsync(uri, null, cancellationToken).ConfigureAwait(false);
if (response != null) if (response != null)
{ {
@ -201,7 +216,7 @@ namespace MapControl
: response.MaxAge.Value : response.MaxAge.Value
}; };
await Cache.SetAsync(cacheKey, buffer, options).ConfigureAwait(false); await Cache.SetAsync(cacheKey, buffer, options, cancellationToken).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
#if WPF #if WPF
using System.Windows.Media; using System.Windows.Media;
@ -73,11 +74,11 @@ namespace MapControl
/// Loads a tile ImageSource asynchronously from GetUri(column, row, zoomLevel). /// Loads a tile ImageSource asynchronously from GetUri(column, row, zoomLevel).
/// This method is called by TileImageLoader when caching is disabled. /// This method is called by TileImageLoader when caching is disabled.
/// </summary> /// </summary>
public virtual Task<ImageSource> LoadImageAsync(int column, int row, int zoomLevel) public virtual Task<ImageSource> LoadImageAsync(int column, int row, int zoomLevel, CancellationToken cancellationToken)
{ {
var uri = GetUri(column, row, zoomLevel); 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);
} }
/// <summary> /// <summary>

View file

@ -5,6 +5,8 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
using System.Threading;
#if WPF #if WPF
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
@ -162,7 +164,7 @@ namespace MapControl
/// <summary> /// <summary>
/// Loads an ImageSource from the URL returned by GetMapRequestUri(). /// Loads an ImageSource from the URL returned by GetMapRequestUri().
/// </summary> /// </summary>
protected override async Task<ImageSource> GetImageAsync(BoundingBox boundingBox, IProgress<double> progress) protected override async Task<ImageSource> GetImageAsync(BoundingBox boundingBox, IProgress<double> progress, CancellationToken cancellationToken)
{ {
ImageSource image = null; ImageSource image = null;
@ -185,7 +187,7 @@ namespace MapControl
if (uri != null) if (uri != null)
{ {
image = await ImageLoader.LoadImageAsync(new Uri(uri), progress); image = await ImageLoader.LoadImageAsync(new Uri(uri), progress, cancellationToken);
} }
} }
else else
@ -208,7 +210,7 @@ namespace MapControl
if (uri1 != null && uri2 != null) 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);
} }
} }
} }

View file

@ -101,7 +101,7 @@ namespace MapControl
{ {
Children.Clear(); Children.Clear();
await LoadTilesAsync(null, null); // stop TileImageLoader CancelLoadTilesAsync();
} }
else if (UpdateChildLayers(tileMatrixSet)) else if (UpdateChildLayers(tileMatrixSet))
{ {

View file

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
@ -45,17 +46,18 @@ namespace MapControl
} }
} }
internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress) internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress, CancellationToken cancellationToken)
{ {
WriteableBitmap mergedBitmap = null; WriteableBitmap mergedBitmap = null;
var p1 = 0d; var p1 = 0d;
var p2 = 0d; var p2 = 0d;
var images = await Task.WhenAll( var images = await Task.WhenAll(
LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })), LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken),
LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); }))); LoadImageAsync(uri2, new Progress<double>(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[0] is BitmapSource bitmap1 &&
images[1] is BitmapSource bitmap2 && images[1] is BitmapSource bitmap2 &&
bitmap1.PixelHeight == bitmap2.PixelHeight && bitmap1.PixelHeight == bitmap2.PixelHeight &&

View file

@ -2,6 +2,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices.WindowsRuntime; using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.Graphics.Imaging; using Windows.Graphics.Imaging;
using Windows.Storage; using Windows.Storage;
@ -66,7 +67,7 @@ namespace MapControl
return image; return image;
} }
internal static async Task<WriteableBitmap> LoadWriteableBitmapAsync(Uri uri, IProgress<double> progress) internal static async Task<WriteableBitmap> LoadWriteableBitmapAsync(Uri uri, IProgress<double> progress, CancellationToken cancellationToken)
{ {
WriteableBitmap bitmap = null; WriteableBitmap bitmap = null;
@ -74,7 +75,7 @@ namespace MapControl
try try
{ {
var response = await GetHttpResponseAsync(uri, progress); var response = await GetHttpResponseAsync(uri, progress, cancellationToken);
if (response?.Buffer != null) if (response?.Buffer != null)
{ {
@ -97,17 +98,18 @@ namespace MapControl
return bitmap; return bitmap;
} }
internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress) internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress, CancellationToken cancellationToken)
{ {
WriteableBitmap mergedBitmap = null; WriteableBitmap mergedBitmap = null;
var p1 = 0d; var p1 = 0d;
var p2 = 0d; var p2 = 0d;
var bitmaps = await Task.WhenAll( var bitmaps = await Task.WhenAll(
LoadWriteableBitmapAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })), LoadWriteableBitmapAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken),
LoadWriteableBitmapAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); }))); LoadWriteableBitmapAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken));
if (bitmaps.Length == 2 && if (!cancellationToken.IsCancellationRequested &&
bitmaps.Length == 2 &&
bitmaps[0] != null && bitmaps[0] != null &&
bitmaps[1] != null && bitmaps[1] != null &&
bitmaps[0].PixelHeight == bitmaps[1].PixelHeight) bitmaps[0].PixelHeight == bitmaps[1].PixelHeight)