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,76 +70,52 @@ 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();
var pendingTiles = new ConcurrentStack<Tile>(tiles.Where(tile => tile.IsPending).Reverse());
var tileCount = pendingTiles.Count;
var taskCount = Math.Min(tileCount, MaxLoadTasks);
if (tileSource != null && tiles != null && (tiles = tiles.Where(tile => tile.IsPending)).Any())
if (taskCount > 0)
{
pendingTiles = new ConcurrentStack<Tile>(tiles.Reverse());
progress?.Report(0d);
var tileCount = pendingTiles.Count;
var taskCount = Math.Min(tileCount, MaxLoadTasks);
if (taskCount > 0)
if (Cache == null || tileSource.UriTemplate == null || !tileSource.UriTemplate.StartsWith("http"))
{
if (Cache == null || tileSource.UriTemplate == null || !tileSource.UriTemplate.StartsWith("http"))
cacheName = null; // disable tile image caching
}
async Task LoadTilesFromQueueAsync()
{
while (!cancellationToken.IsCancellationRequested && pendingTiles.TryPop(out var tile))
{
cacheName = null; // disable tile image caching
}
tile.IsPending = false;
progress?.Report(0d);
progress.Report((double)(tileCount - pendingTiles.Count) / tileCount);
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
try
{
tile.IsPending = false;
progress?.Report((double)(tileCount - tileStack.Count) / tileCount);
try
{
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);
}
await LoadTileImage(tile, tileSource, cacheName, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger?.LogError(ex, "Failed loading tile image {zoom}/{column}/{row}", tile.ZoomLevel, tile.Column, tile.Row);
}
}
for (int i = 0; i < taskCount; i++)
{
tasks[i] = Task.Run(LoadTilesFromQueueAsync);
}
await Task.WhenAll(tasks);
}
var tasks = new Task[taskCount];
for (int i = 0; i < taskCount; i++)
{
tasks[i] = Task.Run(LoadTilesFromQueueAsync, cancellationToken);
}
await Task.WhenAll(tasks);
}
}