mirror of
https://github.com/ClemensFischer/XAML-Map-Control.git
synced 2025-12-06 07:12:04 +01:00
237 lines
8.7 KiB
C#
237 lines
8.7 KiB
C#
using Microsoft.Extensions.Caching.Distributed;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Loads and optionally caches map tile images for a MapTileLayer.
|
|
/// </summary>
|
|
public interface ITileImageLoader
|
|
{
|
|
void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress);
|
|
|
|
void CancelLoadTiles();
|
|
}
|
|
|
|
public partial class TileImageLoader : ITileImageLoader
|
|
{
|
|
private static ILogger logger;
|
|
private static ILogger Logger => logger ?? (logger = ImageLoader.LoggerFactory?.CreateLogger<TileImageLoader>());
|
|
|
|
/// <summary>
|
|
/// Default folder path where a persistent cache implementation may save data, i.e. "C:\ProgramData\MapControl\TileCache".
|
|
/// </summary>
|
|
public static string DefaultCacheFolder =>
|
|
#if UWP
|
|
Path.Combine(Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path, "TileCache");
|
|
#else
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl", "TileCache");
|
|
#endif
|
|
/// <summary>
|
|
/// An IDistributedCache implementation used to cache tile images.
|
|
/// The default value is a MemoryDistributedCache instance.
|
|
/// </summary>
|
|
public static IDistributedCache Cache { get; set; } = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()));
|
|
|
|
/// <summary>
|
|
/// Default expiration time for cached tile images. Used when no expiration time
|
|
/// was transmitted on download. The default value is one day.
|
|
/// </summary>
|
|
public static TimeSpan DefaultCacheExpiration { get; set; } = TimeSpan.FromDays(1);
|
|
|
|
/// <summary>
|
|
/// Minimum expiration time for cached tile images. A transmitted expiration time
|
|
/// that falls below this value is ignored. The default value is TimeSpan.Zero.
|
|
/// </summary>
|
|
public static TimeSpan MinCacheExpiration { get; set; } = TimeSpan.Zero;
|
|
|
|
/// <summary>
|
|
/// Maximum expiration time for cached tile images. A transmitted expiration time
|
|
/// that exceeds this value is ignored. The default value is ten days.
|
|
/// </summary>
|
|
public static TimeSpan MaxCacheExpiration { get; set; } = TimeSpan.FromDays(10);
|
|
|
|
/// <summary>
|
|
/// Maximum number of parallel tile loading tasks. The default value is 4.
|
|
/// </summary>
|
|
public static int MaxLoadTasks { get; set; } = 4;
|
|
|
|
private readonly Queue<Tile> tileQueue = new Queue<Tile>();
|
|
private int taskCount;
|
|
|
|
/// <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 void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress)
|
|
{
|
|
if (Cache == null || tileSource.UriTemplate == null || !tileSource.UriTemplate.StartsWith("http"))
|
|
{
|
|
cacheName = null; // disable caching
|
|
}
|
|
|
|
lock (tileQueue)
|
|
{
|
|
tileQueue.Clear();
|
|
|
|
foreach (var tile in tiles.Where(tile => tile.IsPending))
|
|
{
|
|
tileQueue.Enqueue(tile);
|
|
}
|
|
|
|
var tileCount = tileQueue.Count;
|
|
var maxTasks = Math.Min(tileCount, MaxLoadTasks);
|
|
|
|
while (taskCount < maxTasks)
|
|
{
|
|
taskCount++;
|
|
Logger?.LogDebug("Task count: {count}", taskCount);
|
|
|
|
_ = Task.Run(async () => await LoadTilesFromStack(tileSource, cacheName, tileCount, progress).ConfigureAwait(false));
|
|
}
|
|
}
|
|
}
|
|
|
|
public void CancelLoadTiles()
|
|
{
|
|
lock (tileQueue)
|
|
{
|
|
tileQueue.Clear();
|
|
}
|
|
}
|
|
|
|
private async Task LoadTilesFromStack(TileSource tileSource, string cacheName, int tileCount, IProgress<double> progress)
|
|
{
|
|
while (true)
|
|
{
|
|
Tile tile;
|
|
int tileNumber;
|
|
|
|
lock (tileQueue)
|
|
{
|
|
if (tileQueue.Count == 0)
|
|
{
|
|
taskCount--;
|
|
Logger?.LogDebug("Task count: {count}", taskCount);
|
|
break;
|
|
}
|
|
|
|
tile = tileQueue.Dequeue();
|
|
tileNumber = tileCount - tileQueue.Count;
|
|
}
|
|
|
|
tile.IsPending = false;
|
|
|
|
progress?.Report((double)tileNumber / tileCount);
|
|
|
|
Logger?.LogDebug("Loading tile {number} of {count} ({zoom}/{column}/{row}) in thread {thread}",
|
|
tileNumber, tileCount, tile.ZoomLevel, tile.Column, tile.Row, Environment.CurrentManagedThreadId);
|
|
|
|
try
|
|
{
|
|
await LoadTileImage(tile, tileSource, cacheName).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger?.LogError(ex, "Failed loading tile {zoom}/{column}/{row}", tile.ZoomLevel, tile.Column, tile.Row);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task LoadTileImage(Tile tile, TileSource tileSource, string cacheName)
|
|
{
|
|
// Pass tileSource.LoadImageAsync calls to platform-specific method
|
|
// LoadTileImage(Tile, Func<Task<ImageSource>>) for execution on the UI thread in WinUI and UWP.
|
|
|
|
if (string.IsNullOrEmpty(cacheName))
|
|
{
|
|
Task<ImageSource> LoadImage() => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel);
|
|
|
|
await LoadTileImage(tile, LoadImage).ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
var uri = tileSource.GetUri(tile.Column, tile.Row, tile.ZoomLevel);
|
|
|
|
if (uri != null)
|
|
{
|
|
var buffer = await LoadCachedBuffer(tile, uri, cacheName).ConfigureAwait(false);
|
|
|
|
if (buffer != null && buffer.Length > 0)
|
|
{
|
|
Task<ImageSource> LoadImage() => tileSource.LoadImageAsync(buffer);
|
|
|
|
await LoadTileImage(tile, LoadImage).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task<byte[]> LoadCachedBuffer(Tile tile, Uri uri, string cacheName)
|
|
{
|
|
var extension = Path.GetExtension(uri.LocalPath);
|
|
|
|
if (string.IsNullOrEmpty(extension) || extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
extension = ".jpg";
|
|
}
|
|
|
|
var cacheKey = $"{cacheName}/{tile.ZoomLevel}/{tile.Column}/{tile.Row}{extension}";
|
|
byte[] buffer = null;
|
|
|
|
try
|
|
{
|
|
buffer = await Cache.GetAsync(cacheKey).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger?.LogError(ex, "Cache.GetAsync({cacheKey})", cacheKey);
|
|
}
|
|
|
|
if (buffer == null)
|
|
{
|
|
var response = await ImageLoader.GetHttpResponseAsync(uri).ConfigureAwait(false);
|
|
|
|
if (response != null)
|
|
{
|
|
buffer = response.Buffer ?? Array.Empty<byte>(); // cache even if null, when no tile available
|
|
|
|
try
|
|
{
|
|
var options = new DistributedCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow =
|
|
!response.MaxAge.HasValue ? DefaultCacheExpiration
|
|
: response.MaxAge.Value < MinCacheExpiration ? MinCacheExpiration
|
|
: response.MaxAge.Value > MaxCacheExpiration ? MaxCacheExpiration
|
|
: response.MaxAge.Value
|
|
};
|
|
|
|
await Cache.SetAsync(cacheKey, buffer, options).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger?.LogError(ex, "Cache.SetAsync({cacheKey})", cacheKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
}
|
|
}
|