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() protected async Task UpdateImageAsync()
{ {
updateTimer.Stop(); updateTimer.Stop();
cancellationTokenSource?.Cancel();
if (cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
cancellationTokenSource = null;
}
if (ParentMap != null && ParentMap.ActualWidth > 0d && ParentMap.ActualHeight > 0d) 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)); 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; cancellationTokenSource = null;

View file

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
#if WPF #if WPF
using System.Windows; using System.Windows;
@ -60,6 +62,7 @@ namespace MapControl
private readonly Progress<double> loadingProgress; private readonly Progress<double> loadingProgress;
private readonly DispatcherTimer updateTimer; private readonly DispatcherTimer updateTimer;
private ITileImageLoader tileImageLoader; private ITileImageLoader tileImageLoader;
private CancellationTokenSource cancellationTokenSource;
private MapBase parentMap; private MapBase parentMap;
protected MapTileLayerBase() protected MapTileLayerBase()
@ -191,14 +194,24 @@ namespace MapControl
protected bool IsBaseMapLayer => parentMap != null && parentMap.Children.Count > 0 && parentMap.Children[0] == this; 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() protected void CancelLoadTiles()
{ {
TileImageLoader.CancelLoadTiles(); cancellationTokenSource?.Cancel();
ClearValue(LoadingProgressProperty); ClearValue(LoadingProgressProperty);
} }

View file

@ -9,7 +9,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading; using System.Threading;
#if WPF #if WPF
using System.Windows.Media; using System.Windows.Media;
#elif UWP #elif UWP
@ -25,9 +24,7 @@ namespace MapControl
/// </summary> /// </summary>
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, CancellationToken cancellationToken);
void CancelLoadTiles();
} }
public partial class TileImageLoader : ITileImageLoader public partial class TileImageLoader : ITileImageLoader
@ -73,76 +70,52 @@ namespace MapControl
private static ILogger logger; private static ILogger logger;
private static ILogger Logger => logger ?? (logger = ImageLoader.LoggerFactory?.CreateLogger<TileImageLoader>()); 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> /// <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 not 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, 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; if (Cache == null || tileSource.UriTemplate == null || !tileSource.UriTemplate.StartsWith("http"))
var taskCount = Math.Min(tileCount, MaxLoadTasks);
if (taskCount > 0)
{ {
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]; try
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; await LoadTileImage(tile, tileSource, cacheName, cancellationToken).ConfigureAwait(false);
}
progress?.Report((double)(tileCount - tileStack.Count) / tileCount); catch (Exception ex)
{
try Logger?.LogError(ex, "Failed loading tile image {zoom}/{column}/{row}", tile.ZoomLevel, tile.Column, tile.Row);
{
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);
}
} }
} }
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);
} }
} }