Reverted image download cancellation

This commit is contained in:
ClemensFischer 2025-08-21 22:40:51 +02:00
parent 6f82f8012a
commit 2cf8636f4c
7 changed files with 86 additions and 131 deletions

View file

@ -4,7 +4,6 @@ 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
@ -39,18 +38,17 @@ namespace MapControl
} }
} }
internal static async Task<IImage> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress, CancellationToken cancellationToken) internal static async Task<IImage> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
{ {
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); }), cancellationToken), LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken)); LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
if (!cancellationToken.IsCancellationRequested && if (images.Length == 2 &&
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

@ -2,7 +2,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
#if WPF #if WPF
using System.Windows.Media; using System.Windows.Media;
@ -28,16 +27,11 @@ namespace MapControl
static ImageLoader() static ImageLoader()
{ {
HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
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 Task<ImageSource> LoadImageAsync(Uri uri, IProgress<double> progress = null) public static async Task<ImageSource> LoadImageAsync(Uri uri, IProgress<double> progress = null)
{
return LoadImageAsync(uri, progress, CancellationToken.None);
}
public static async Task<ImageSource> LoadImageAsync(Uri uri, IProgress<double> progress, CancellationToken cancellationToken)
{ {
ImageSource image = null; ImageSource image = null;
@ -47,7 +41,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, cancellationToken); var response = await GetHttpResponseAsync(uri, progress);
if (response?.Buffer != null) if (response?.Buffer != null)
{ {
@ -93,19 +87,26 @@ namespace MapControl
} }
} }
internal static async Task<HttpResponse> GetHttpResponseAsync(Uri uri, IProgress<double> progress, CancellationToken cancellationToken) internal static async Task<HttpResponse> GetHttpResponseAsync(Uri uri, IProgress<double> progress = null)
{ {
HttpResponse response = null; HttpResponse response = null;
try try
{ {
var completionOptions = progress != null ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead; using (var responseMessage = await HttpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
using (var responseMessage = await HttpClient.GetAsync(uri, completionOptions, cancellationToken).ConfigureAwait(false))
{ {
if (responseMessage.IsSuccessStatusCode) if (responseMessage.IsSuccessStatusCode)
{ {
byte[] buffer = await responseMessage.Content.ReadAsByteArrayAsync(progress, cancellationToken).ConfigureAwait(false); byte[] buffer;
if (progress != null && responseMessage.Content.Headers.ContentLength.HasValue)
{
buffer = await ReadAsByteArray(responseMessage.Content, progress).ConfigureAwait(false);
}
else
{
buffer = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
response = new HttpResponse(buffer, responseMessage.Headers.CacheControl?.MaxAge); response = new HttpResponse(buffer, responseMessage.Headers.CacheControl?.MaxAge);
} }
@ -115,17 +116,6 @@ namespace MapControl
} }
} }
} }
catch (OperationCanceledException ex)
{
if (ex.CancellationToken.IsCancellationRequested)
{
Logger?.LogTrace("Cancelled loading image from {uri}", uri);
}
else
{
Logger?.LogError(ex, "Failed 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);
@ -133,60 +123,30 @@ namespace MapControl
return response; return response;
} }
}
internal static class HttpContentExtensions private static async Task<byte[]> ReadAsByteArray(HttpContent content, IProgress<double> progress)
{
public static async Task<byte[]> ReadAsByteArrayAsync(this HttpContent content, IProgress<double> progress, CancellationToken cancellationToken)
{ {
byte[] buffer; var length = (int)content.Headers.ContentLength.Value;
var buffer = new byte[length];
if (progress != null && content.Headers.ContentLength.HasValue) using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false))
{ {
var length = (int)content.Headers.ContentLength.Value; int offset = 0;
buffer = new byte[length]; int read;
using (var stream = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) while (offset < length &&
(read = await stream.ReadAsync(buffer, offset, length - offset).ConfigureAwait(false)) > 0)
{ {
int offset = 0; offset += read;
int read;
while (offset < length && if (offset < length) // 1.0 reported by caller
(read = await stream.ReadAsync(buffer, offset, length - offset, cancellationToken).ConfigureAwait(false)) > 0)
{ {
cancellationToken.ThrowIfCancellationRequested(); progress.Report((double)offset / length);
offset += read;
if (offset < length) // 1.0 reported by caller
{
progress.Report((double)offset / length);
}
} }
} }
} }
else
{
buffer = await content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
}
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
} }
} }

View file

@ -1,6 +1,5 @@
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;
@ -54,7 +53,7 @@ namespace MapControl
private readonly Progress<double> loadingProgress; private readonly Progress<double> loadingProgress;
private readonly DispatcherTimer updateTimer; private readonly DispatcherTimer updateTimer;
private CancellationTokenSource cancellationTokenSource; private bool updateInProgress;
public MapImageLayer() public MapImageLayer()
{ {
@ -166,38 +165,39 @@ namespace MapControl
} }
} }
protected abstract Task<ImageSource> GetImageAsync(BoundingBox boundingBox, IProgress<double> progress, CancellationToken cancellationToken); protected abstract Task<ImageSource> GetImageAsync(BoundingBox boundingBox, IProgress<double> progress);
protected async Task UpdateImageAsync() protected async Task UpdateImageAsync()
{ {
updateTimer.Stop(); if (updateInProgress)
cancellationTokenSource?.Cancel();
ClearValue(LoadingProgressProperty);
if (ParentMap != null && ParentMap.ActualWidth > 0d && ParentMap.ActualHeight > 0d)
{ {
var width = ParentMap.ActualWidth * RelativeImageSize; // Update image on next tick, start timer if not running.
var height = ParentMap.ActualHeight * RelativeImageSize; //
var x = (ParentMap.ActualWidth - width) / 2d; updateTimer.Run();
var y = (ParentMap.ActualHeight - height) / 2d; }
else
{
updateInProgress = true;
updateTimer.Stop();
var boundingBox = ParentMap.ViewRectToBoundingBox(new Rect(x, y, width, height)); ImageSource image = null;
BoundingBox boundingBox = null;
ImageSource image; if (ParentMap != null && ParentMap.ActualWidth > 0d && ParentMap.ActualHeight > 0d)
using (cancellationTokenSource = new CancellationTokenSource())
{ {
image = await GetImageAsync(boundingBox, loadingProgress, cancellationTokenSource.Token); var width = ParentMap.ActualWidth * RelativeImageSize;
var height = ParentMap.ActualHeight * RelativeImageSize;
var x = (ParentMap.ActualWidth - width) / 2d;
var y = (ParentMap.ActualHeight - height) / 2d;
boundingBox = ParentMap.ViewRectToBoundingBox(new Rect(x, y, width, height));
image = await GetImageAsync(boundingBox, loadingProgress);
} }
cancellationTokenSource = null; SwapImages(image, boundingBox);
if (image != null && Children.Count >= 2) updateInProgress = false;
{
SwapImages(image, boundingBox);
}
} }
} }
@ -212,22 +212,25 @@ namespace MapControl
private void SwapImages(ImageSource image, BoundingBox boundingBox) private void SwapImages(ImageSource image, BoundingBox boundingBox)
{ {
var topImage = (Image)Children[0]; if (Children.Count >= 2)
Children.RemoveAt(0);
Children.Insert(1, topImage);
topImage.Source = image;
SetBoundingBox(topImage, boundingBox);
if (MapBase.ImageFadeDuration > TimeSpan.Zero)
{ {
FadeOver(); var topImage = (Image)Children[0];
}
else Children.RemoveAt(0);
{ Children.Insert(1, topImage);
topImage.Opacity = 1d;
Children[0].Opacity = 0d; topImage.Source = image;
SetBoundingBox(topImage, boundingBox);
if (MapBase.ImageFadeDuration > TimeSpan.Zero)
{
FadeOver();
}
else
{
topImage.Opacity = 1d;
Children[0].Opacity = 0d;
}
} }
} }
} }

View file

@ -192,7 +192,7 @@ namespace MapControl
if (buffer == null) if (buffer == null)
{ {
var response = await ImageLoader.GetHttpResponseAsync(uri, null, CancellationToken.None).ConfigureAwait(false); var response = await ImageLoader.GetHttpResponseAsync(uri).ConfigureAwait(false);
if (response != null) if (response != null)
{ {

View file

@ -5,8 +5,6 @@ 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;
@ -164,7 +162,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, CancellationToken cancellationToken) protected override async Task<ImageSource> GetImageAsync(BoundingBox boundingBox, IProgress<double> progress)
{ {
ImageSource image = null; ImageSource image = null;
@ -187,7 +185,7 @@ namespace MapControl
if (uri != null) if (uri != null)
{ {
image = await ImageLoader.LoadImageAsync(new Uri(uri), progress, cancellationToken); image = await ImageLoader.LoadImageAsync(new Uri(uri), progress);
} }
} }
else else
@ -210,7 +208,7 @@ namespace MapControl
if (uri1 != null && uri2 != null) if (uri1 != null && uri2 != null)
{ {
image = await ImageLoader.LoadMergedImageAsync(new Uri(uri1), new Uri(uri2), progress, cancellationToken); image = await ImageLoader.LoadMergedImageAsync(new Uri(uri1), new Uri(uri2), progress);
} }
} }
} }

View file

@ -1,6 +1,5 @@
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;
@ -46,18 +45,17 @@ namespace MapControl
} }
} }
internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress, CancellationToken cancellationToken) internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
{ {
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); }), cancellationToken), LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken)); LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
if (!cancellationToken.IsCancellationRequested && if (images.Length == 2 &&
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,7 +2,6 @@
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;
@ -67,7 +66,7 @@ namespace MapControl
return image; return image;
} }
internal static async Task<WriteableBitmap> LoadWriteableBitmapAsync(Uri uri, IProgress<double> progress, CancellationToken cancellationToken) internal static async Task<WriteableBitmap> LoadWriteableBitmapAsync(Uri uri, IProgress<double> progress)
{ {
WriteableBitmap bitmap = null; WriteableBitmap bitmap = null;
@ -75,7 +74,7 @@ namespace MapControl
try try
{ {
var response = await GetHttpResponseAsync(uri, progress, cancellationToken); var response = await GetHttpResponseAsync(uri, progress);
if (response?.Buffer != null) if (response?.Buffer != null)
{ {
@ -98,18 +97,17 @@ namespace MapControl
return bitmap; return bitmap;
} }
internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress, CancellationToken cancellationToken) internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
{ {
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); }), cancellationToken), LoadWriteableBitmapAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
LoadWriteableBitmapAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); }), cancellationToken)); LoadWriteableBitmapAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
if (!cancellationToken.IsCancellationRequested && if (bitmaps.Length == 2 &&
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)