Added overridable method TileImageLoader.LoadImageAsync(byte[])

This commit is contained in:
ClemensFischer 2025-01-16 19:57:00 +01:00
parent c7b0bcb1c1
commit fa160ff138
3 changed files with 80 additions and 77 deletions

View file

@ -12,6 +12,13 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
#if WPF
using System.Windows.Media;
#elif UWP
using Windows.UI.Xaml.Media;
#elif WINUI
using Microsoft.UI.Xaml.Media;
#endif
namespace MapControl namespace MapControl
{ {
@ -54,13 +61,11 @@ namespace MapControl
/// </summary> /// </summary>
public static int MaxLoadTasks { get; set; } = 4; public static int MaxLoadTasks { get; set; } = 4;
private ConcurrentStack<Tile> pendingTiles; private ConcurrentStack<Tile> pendingTiles;
/// <summary> /// <summary>
/// Loads all pending tiles from the tiles collection. /// Loads all pending tiles from the tiles collection. Tile image caching is enabled when the Cache
/// If tileSource.UriFormat starts with "http" and cacheName is a non-empty string, /// property is non-null and tileSource.UriFormat starts with "http" and cacheName is a non-empty string.
/// tile images will be cached in the TileImageLoader's Cache - if that is not null.
/// </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)
{ {
@ -77,7 +82,7 @@ namespace MapControl
{ {
if (Cache == null || tileSource.UriTemplate == null || !tileSource.UriTemplate.StartsWith("http")) if (Cache == null || tileSource.UriTemplate == null || !tileSource.UriTemplate.StartsWith("http"))
{ {
cacheName = null; // no tile caching cacheName = null; // disable tile image caching
} }
progress?.Report(0d); progress?.Report(0d);
@ -93,7 +98,14 @@ namespace MapControl
progress?.Report((double)(tileCount - tileStack.Count) / tileCount); progress?.Report((double)(tileCount - tileStack.Count) / tileCount);
await LoadTileAsync(tile, tileSource, cacheName).ConfigureAwait(false); try
{
await LoadTileAsync(tile, tileSource, cacheName).ConfigureAwait(false);
}
catch (Exception ex)
{
Debug.WriteLine($"{nameof(TileImageLoader)}: {tile.ZoomLevel}/{tile.Column}/{tile.Row}: {ex.Message}");
}
} }
} }
@ -107,32 +119,36 @@ namespace MapControl
} }
} }
private static async Task LoadTileAsync(Tile tile, TileSource tileSource, string cacheName) protected virtual Task<ImageSource> LoadImageAsync(byte[] buffer) => ImageLoader.LoadImageAsync(buffer);
{
try
{
if (string.IsNullOrEmpty(cacheName))
{
await LoadTileAsync(tile, () => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel));
}
else
{
var uri = tileSource.GetUri(tile.Column, tile.Row, tile.ZoomLevel);
if (uri != null) private async Task LoadTileAsync(Tile tile, TileSource tileSource, string cacheName)
{ {
await LoadCachedTileAsync(tile, uri, cacheName); Func<Task<ImageSource>> loadImageFunc;
}
} if (string.IsNullOrEmpty(cacheName))
}
catch (Exception ex)
{ {
Debug.WriteLine($"{nameof(TileImageLoader)}: {tile.ZoomLevel}/{tile.Column}/{tile.Row}: {ex.Message}"); loadImageFunc = () => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel);
} }
else
{
var uri = tileSource.GetUri(tile.Column, tile.Row, tile.ZoomLevel);
if (uri == null) return;
var buffer = await LoadCachedBufferAsync(tile, uri, cacheName).ConfigureAwait(false);
if (buffer == null || buffer.Length == 0) return;
loadImageFunc = () => LoadImageAsync(buffer);
}
await LoadTileAsync(tile, loadImageFunc).ConfigureAwait(false); // loadImageFunc runs in UI thread in WinUI/UWP
} }
private static async Task LoadCachedTileAsync(Tile tile, Uri uri, string cacheName) private static async Task<byte[]> LoadCachedBufferAsync(Tile tile, Uri uri, string cacheName)
{ {
byte[] buffer = null;
var extension = Path.GetExtension(uri.LocalPath); var extension = Path.GetExtension(uri.LocalPath);
if (string.IsNullOrEmpty(extension) || extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)) if (string.IsNullOrEmpty(extension) || extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
@ -142,7 +158,14 @@ namespace MapControl
var cacheKey = $"{cacheName}/{tile.ZoomLevel}/{tile.Column}/{tile.Row}{extension}"; var cacheKey = $"{cacheName}/{tile.ZoomLevel}/{tile.Column}/{tile.Row}{extension}";
var buffer = await ReadCacheAsync(cacheKey).ConfigureAwait(false); try
{
buffer = await Cache.GetAsync(cacheKey);
}
catch (Exception ex)
{
Debug.WriteLine($"{nameof(TileImageLoader)}.{nameof(Cache)}.{nameof(Cache.GetAsync)}: {cacheKey}: {ex.Message}");
}
if (buffer == null) if (buffer == null)
{ {
@ -152,51 +175,24 @@ namespace MapControl
{ {
buffer = response.Buffer; buffer = response.Buffer;
await WriteCacheAsync(cacheKey, buffer, response.MaxAge).ConfigureAwait(false); try
{
var expiration = !response.MaxAge.HasValue ? DefaultCacheExpiration
: response.MaxAge.Value > MaxCacheExpiration ? MaxCacheExpiration
: response.MaxAge.Value;
await Cache.SetAsync(cacheKey,
buffer ?? Array.Empty<byte>(), // cache even if null, when no tile available
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiration });
}
catch (Exception ex)
{
Debug.WriteLine($"{nameof(TileImageLoader)}.{nameof(Cache)}.{nameof(Cache.SetAsync)}: {cacheKey}: {ex.Message}");
}
} }
} }
if (buffer != null && buffer.Length > 0) return buffer;
{
await LoadTileAsync(tile, () => ImageLoader.LoadImageAsync(buffer)).ConfigureAwait(false);
}
}
private static async Task<byte[]> ReadCacheAsync(string cacheKey)
{
try
{
return await Cache.GetAsync(cacheKey);
}
catch (Exception ex)
{
Debug.WriteLine($"{nameof(TileImageLoader)}.{nameof(Cache)}.{nameof(Cache.GetAsync)}: {cacheKey}: {ex.Message}");
return null;
}
}
private static async Task WriteCacheAsync(string cacheKey, byte[] buffer, TimeSpan? expiration)
{
if (!expiration.HasValue)
{
expiration = DefaultCacheExpiration;
}
else if (expiration > MaxCacheExpiration)
{
expiration = MaxCacheExpiration;
}
try
{
await Cache.SetAsync(cacheKey,
buffer ?? Array.Empty<byte>(), // cache even if null, when no tile available
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiration });
}
catch (Exception ex)
{
Debug.WriteLine($"{nameof(TileImageLoader)}.{nameof(Cache)}.{nameof(Cache.SetAsync)}: {cacheKey}: {ex.Message}");
}
} }
} }
} }

View file

@ -11,15 +11,17 @@ namespace MapControl
{ {
public partial class TileImageLoader public partial class TileImageLoader
{ {
private static async Task LoadTileAsync(Tile tile, Func<Task<ImageSource>> loadImageFunc) private static Task<object> LoadTileAsync(Tile tile, Func<Task<ImageSource>> loadImageFunc)
{ {
var tcs = new TaskCompletionSource<object>(); var tcs = new TaskCompletionSource<object>();
async void callback() async void LoadTileImage()
{ {
try try
{ {
tile.SetImageSource(await loadImageFunc()); var image = await loadImageFunc();
tile.SetImageSource(image);
tcs.TrySetResult(null); tcs.TrySetResult(null);
} }
catch (Exception ex) catch (Exception ex)
@ -28,9 +30,9 @@ namespace MapControl
} }
} }
await tile.Image.Dispatcher.RunAsync(CoreDispatcherPriority.Low, callback); _ = tile.Image.Dispatcher.RunAsync(CoreDispatcherPriority.Low, LoadTileImage);
await tcs.Task.ConfigureAwait(false); return tcs.Task;
} }
} }
} }

View file

@ -15,11 +15,13 @@ namespace MapControl
{ {
var tcs = new TaskCompletionSource(); var tcs = new TaskCompletionSource();
async void callback() async void LoadTileImage()
{ {
try try
{ {
tile.SetImageSource(await loadImageFunc()); var image = await loadImageFunc();
tile.SetImageSource(image);
tcs.TrySetResult(); tcs.TrySetResult();
} }
catch (Exception ex) catch (Exception ex)
@ -28,7 +30,10 @@ namespace MapControl
} }
} }
tile.Image.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, callback); if (!tile.Image.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, LoadTileImage))
{
tcs.TrySetCanceled();
}
return tcs.Task; return tcs.Task;
} }