mirror of
https://github.com/ClemensFischer/XAML-Map-Control.git
synced 2025-12-06 07:12:04 +01:00
Added optional image request cancellation
This commit is contained in:
parent
ebd90f5a45
commit
34fda668c9
|
|
@ -1,17 +1,21 @@
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
{
|
{
|
||||||
public partial class TileImageLoader
|
public partial class TileImageLoader
|
||||||
{
|
{
|
||||||
private static async Task LoadTileImage(Tile tile, Func<Task<IImage>> loadImageFunc)
|
private static async Task LoadTileImage(Tile tile, Func<Task<IImage>> loadImageFunc, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var image = await loadImageFunc().ConfigureAwait(false);
|
var image = await loadImageFunc().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
_ = Dispatcher.UIThread.InvokeAsync(() => tile.SetImageSource(image)); // no need to await InvokeAsync
|
_ = Dispatcher.UIThread.InvokeAsync(() => tile.SetImageSource(image)); // no need to await InvokeAsync
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,11 +103,11 @@ namespace MapControl
|
||||||
|
|
||||||
if (progress != null && responseMessage.Content.Headers.ContentLength.HasValue)
|
if (progress != null && responseMessage.Content.Headers.ContentLength.HasValue)
|
||||||
{
|
{
|
||||||
buffer = await ReadAsByteArray(responseMessage.Content, progress).ConfigureAwait(false);
|
buffer = await responseMessage.Content.ReadAsByteArrayAsync(progress, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
buffer = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
buffer = await responseMessage.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
response = new HttpResponse(buffer, responseMessage.Headers.CacheControl?.MaxAge);
|
response = new HttpResponse(buffer, responseMessage.Headers.CacheControl?.MaxAge);
|
||||||
|
|
@ -129,20 +129,25 @@ namespace MapControl
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<byte[]> ReadAsByteArray(HttpContent content, IProgress<double> progress)
|
internal static class HttpContentExtensions
|
||||||
|
{
|
||||||
|
public static async Task<byte[]> ReadAsByteArrayAsync(this HttpContent content, IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var length = (int)content.Headers.ContentLength.Value;
|
var length = (int)content.Headers.ContentLength.Value;
|
||||||
var buffer = new byte[length];
|
var buffer = new byte[length];
|
||||||
|
|
||||||
using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false))
|
using (var stream = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
int read;
|
int read;
|
||||||
|
|
||||||
while (offset < length &&
|
while (offset < length &&
|
||||||
(read = await stream.ReadAsync(buffer, offset, length - offset).ConfigureAwait(false)) > 0)
|
(read = await stream.ReadAsync(buffer, offset, length - offset, cancellationToken).ConfigureAwait(false)) > 0)
|
||||||
{
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
offset += read;
|
offset += read;
|
||||||
|
|
||||||
if (offset < length) // 1.0 reported by caller
|
if (offset < length) // 1.0 reported by caller
|
||||||
|
|
@ -154,5 +159,21 @@ namespace MapControl
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !NET
|
||||||
|
public static Task<byte[]> ReadAsByteArrayAsync(this HttpContent content, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
return content.ReadAsByteArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<Stream> ReadAsStreamAsync(this HttpContent content, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
return content.ReadAsStreamAsync();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,8 +7,8 @@ using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
#if WPF
|
#if WPF
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
#elif UWP
|
#elif UWP
|
||||||
|
|
@ -67,6 +67,13 @@ namespace MapControl
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static int MaxLoadTasks { get; set; } = 4;
|
public static int MaxLoadTasks { get; set; } = 4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether HTTP requests are cancelled when the LoadTilesAsync method is cancelled.
|
||||||
|
/// If the property value is false, cancellation only stops dequeuing entries from the tile queue,
|
||||||
|
/// but lets currently running requests run to completion.
|
||||||
|
/// </summary>
|
||||||
|
public static bool RequestCancellationEnabled { get; set; }
|
||||||
|
|
||||||
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>());
|
||||||
|
|
||||||
|
|
@ -82,8 +89,6 @@ namespace MapControl
|
||||||
|
|
||||||
if (taskCount > 0)
|
if (taskCount > 0)
|
||||||
{
|
{
|
||||||
progress?.Report(0d);
|
|
||||||
|
|
||||||
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
|
cacheName = null; // disable tile image caching
|
||||||
|
|
@ -95,19 +100,25 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
tile.IsPending = false;
|
tile.IsPending = false;
|
||||||
|
|
||||||
progress.Report((double)(tileCount - pendingTiles.Count) / tileCount);
|
progress?.Report((double)(tileCount - pendingTiles.Count) / tileCount);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await LoadTileImage(tile, tileSource, cacheName, cancellationToken).ConfigureAwait(false);
|
var requestCancellationToken = RequestCancellationEnabled ? cancellationToken : CancellationToken.None;
|
||||||
|
|
||||||
|
await LoadTileImage(tile, tileSource, cacheName, requestCancellationToken).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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tile.IsPending = cancellationToken.IsCancellationRequested && tile.Image == null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
var tasks = new Task[taskCount];
|
var tasks = new Task[taskCount];
|
||||||
|
|
||||||
for (int i = 0; i < taskCount; i++)
|
for (int i = 0; i < taskCount; i++)
|
||||||
|
|
@ -115,9 +126,24 @@ namespace MapControl
|
||||||
tasks[i] = Task.Run(LoadTilesFromQueueAsync, cancellationToken);
|
tasks[i] = Task.Run(LoadTilesFromQueueAsync, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
progress?.Report(0d);
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
await Task.WhenAll(tasks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// no action
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Logger?.LogTrace("Cancelled LoadTilesAsync with {count} queued tiles", pendingTiles.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task LoadTileImage(Tile tile, TileSource tileSource, string cacheName, CancellationToken cancellationToken)
|
private static async Task LoadTileImage(Tile tile, TileSource tileSource, string cacheName, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|
@ -128,7 +154,7 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
Task<ImageSource> LoadImage() => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel, cancellationToken);
|
Task<ImageSource> LoadImage() => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel, cancellationToken);
|
||||||
|
|
||||||
await LoadTileImage(tile, LoadImage).ConfigureAwait(false);
|
await LoadTileImage(tile, LoadImage, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -142,7 +168,7 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
Task<ImageSource> LoadImage() => tileSource.LoadImageAsync(buffer);
|
Task<ImageSource> LoadImage() => tileSource.LoadImageAsync(buffer);
|
||||||
|
|
||||||
await LoadTileImage(tile, LoadImage).ConfigureAwait(false);
|
await LoadTileImage(tile, LoadImage, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,9 +191,13 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
buffer = await Cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
|
buffer = await Cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger?.LogTrace("Cancelled Cache.GetAsync({cacheKey})", cacheKey);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger?.LogError(ex, "Cache.GetAsync: {cacheKey}", cacheKey);
|
Logger?.LogError(ex, "Cache.GetAsync({cacheKey})", cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer == null)
|
if (buffer == null)
|
||||||
|
|
@ -191,9 +221,13 @@ namespace MapControl
|
||||||
|
|
||||||
await Cache.SetAsync(cacheKey, buffer, options, cancellationToken).ConfigureAwait(false);
|
await Cache.SetAsync(cacheKey, buffer, options, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger?.LogTrace("Cancelled Cache.SetAsync({cacheKey})", cacheKey);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger?.LogError(ex, "Cache.SetAsync: {cacheKey}", cacheKey);
|
Logger?.LogError(ex, "Cache.SetAsync({cacheKey})", cacheKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Windows.UI.Core;
|
using Windows.UI.Core;
|
||||||
using Windows.UI.Xaml.Media;
|
using Windows.UI.Xaml.Media;
|
||||||
|
|
@ -7,7 +8,7 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
public partial class TileImageLoader
|
public partial class TileImageLoader
|
||||||
{
|
{
|
||||||
private static async Task LoadTileImage(Tile tile, Func<Task<ImageSource>> loadImageFunc)
|
private static async Task LoadTileImage(Tile tile, Func<Task<ImageSource>> loadImageFunc, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<object>();
|
var tcs = new TaskCompletionSource<object>();
|
||||||
|
|
||||||
|
|
@ -19,8 +20,11 @@ namespace MapControl
|
||||||
|
|
||||||
tcs.TrySetResult(null); // tcs.Task has completed when image is loaded
|
tcs.TrySetResult(null); // tcs.Task has completed when image is loaded
|
||||||
|
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
tile.SetImageSource(image);
|
tile.SetImageSource(image);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
tcs.TrySetException(ex);
|
tcs.TrySetException(ex);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
|
@ -6,11 +7,14 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
public partial class TileImageLoader
|
public partial class TileImageLoader
|
||||||
{
|
{
|
||||||
private static async Task LoadTileImage(Tile tile, Func<Task<ImageSource>> loadImageFunc)
|
private static async Task LoadTileImage(Tile tile, Func<Task<ImageSource>> loadImageFunc, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var image = await loadImageFunc().ConfigureAwait(false);
|
var image = await loadImageFunc().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
_ = tile.Image.Dispatcher.InvokeAsync(() => tile.SetImageSource(image)); // no need to await InvokeAsync
|
_ = tile.Image.Dispatcher.InvokeAsync(() => tile.SetImageSource(image)); // no need to await InvokeAsync
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
{
|
{
|
||||||
public partial class TileImageLoader
|
public partial class TileImageLoader
|
||||||
{
|
{
|
||||||
private static Task LoadTileImage(Tile tile, Func<Task<ImageSource>> loadImageFunc)
|
private static Task LoadTileImage(Tile tile, Func<Task<ImageSource>> loadImageFunc, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource();
|
var tcs = new TaskCompletionSource();
|
||||||
|
|
||||||
|
|
@ -19,8 +20,11 @@ namespace MapControl
|
||||||
|
|
||||||
tcs.TrySetResult(); // tcs.Task has completed when image is loaded
|
tcs.TrySetResult(); // tcs.Task has completed when image is loaded
|
||||||
|
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
tile.SetImageSource(image);
|
tile.SetImageSource(image);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
tcs.TrySetException(ex);
|
tcs.TrySetException(ex);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue