Image loading with cancellation

This commit is contained in:
ClemensFischer 2025-08-20 00:29:56 +02:00
parent 79b9e9d33d
commit ebd90f5a45
3 changed files with 53 additions and 69 deletions

View file

@ -171,12 +171,7 @@ namespace MapControl
protected async Task UpdateImageAsync()
{
updateTimer.Stop();
if (cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
cancellationTokenSource = null;
}
cancellationTokenSource?.Cancel();
if (ParentMap != null && ParentMap.ActualWidth > 0d && ParentMap.ActualHeight > 0d)
{
@ -187,9 +182,12 @@ namespace MapControl
var boundingBox = ParentMap.ViewRectToBoundingBox(new Rect(x, y, width, height));
cancellationTokenSource = new CancellationTokenSource();
ImageSource image;
var image = await GetImageAsync(boundingBox, loadingProgress, cancellationTokenSource.Token);
using (cancellationTokenSource = new CancellationTokenSource())
{
image = await GetImageAsync(boundingBox, loadingProgress, cancellationTokenSource.Token);
}
cancellationTokenSource = null;

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#if WPF
using System.Windows;
@ -60,6 +62,7 @@ namespace MapControl
private readonly Progress<double> loadingProgress;
private readonly DispatcherTimer updateTimer;
private ITileImageLoader tileImageLoader;
private CancellationTokenSource cancellationTokenSource;
private MapBase parentMap;
protected MapTileLayerBase()
@ -191,14 +194,24 @@ namespace MapControl
protected bool IsBaseMapLayer => parentMap != null && parentMap.Children.Count > 0 && parentMap.Children[0] == this;
protected Task LoadTilesAsync(IEnumerable<Tile> tiles, string cacheName)
protected async Task LoadTilesAsync(IEnumerable<Tile> tiles, string cacheName)
{
return TileImageLoader.LoadTilesAsync(tiles, TileSource, cacheName, loadingProgress);
cancellationTokenSource?.Cancel();
if (TileSource != null && tiles != null && tiles.Any(tile => tile.IsPending))
{
using (cancellationTokenSource = new CancellationTokenSource())
{
await TileImageLoader.LoadTilesAsync(tiles, TileSource, cacheName, loadingProgress, cancellationTokenSource.Token);
}
cancellationTokenSource = null;
}
}
protected void CancelLoadTiles()
{
TileImageLoader.CancelLoadTiles();
cancellationTokenSource?.Cancel();
ClearValue(LoadingProgressProperty);
}

View file

@ -9,7 +9,6 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
#if WPF
using System.Windows.Media;
#elif UWP
@ -25,9 +24,7 @@ namespace MapControl
/// </summary>
public interface ITileImageLoader
{
Task LoadTilesAsync(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress);
void CancelLoadTiles();
Task LoadTilesAsync(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress, CancellationToken cancellationToken);
}
public partial class TileImageLoader : ITileImageLoader
@ -73,61 +70,36 @@ namespace MapControl
private static ILogger logger;
private static ILogger Logger => logger ?? (logger = ImageLoader.LoggerFactory?.CreateLogger<TileImageLoader>());
private ConcurrentStack<Tile> pendingTiles = new ConcurrentStack<Tile>();
private CancellationTokenSource cancellationTokenSource;
public void CancelLoadTiles()
{
pendingTiles.Clear();
if (cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
cancellationTokenSource = null;
}
}
/// <summary>
/// Loads all pending tiles from the tiles collection. Tile image caching is enabled when the Cache
/// property is not null and tileSource.UriFormat starts with "http" and cacheName is a non-empty string.
/// </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, CancellationToken cancellationToken)
{
CancelLoadTiles();
if (tileSource != null && tiles != null && (tiles = tiles.Where(tile => tile.IsPending)).Any())
{
pendingTiles = new ConcurrentStack<Tile>(tiles.Reverse());
var pendingTiles = new ConcurrentStack<Tile>(tiles.Where(tile => tile.IsPending).Reverse());
var tileCount = pendingTiles.Count;
var taskCount = Math.Min(tileCount, MaxLoadTasks);
if (taskCount > 0)
{
progress?.Report(0d);
if (Cache == null || tileSource.UriTemplate == null || !tileSource.UriTemplate.StartsWith("http"))
{
cacheName = null; // disable tile image caching
}
progress?.Report(0d);
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
while (!cancellationToken.IsCancellationRequested && pendingTiles.TryPop(out var tile))
{
tile.IsPending = false;
progress?.Report((double)(tileCount - tileStack.Count) / tileCount);
progress.Report((double)(tileCount - pendingTiles.Count) / tileCount);
try
{
await LoadTileImage(tile, tileSource, cacheName, cancellationTokenSource.Token).ConfigureAwait(false);
await LoadTileImage(tile, tileSource, cacheName, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@ -136,15 +108,16 @@ namespace MapControl
}
}
var tasks = new Task[taskCount];
for (int i = 0; i < taskCount; i++)
{
tasks[i] = Task.Run(LoadTilesFromQueueAsync);
tasks[i] = Task.Run(LoadTilesFromQueueAsync, cancellationToken);
}
await Task.WhenAll(tasks);
}
}
}
private static async Task LoadTileImage(Tile tile, TileSource tileSource, string cacheName, CancellationToken cancellationToken)
{