diff --git a/MBTiles/Avalonia/MBTiles.Avalonia.csproj b/MBTiles/Avalonia/MBTiles.Avalonia.csproj
index fe132b41..51422504 100644
--- a/MBTiles/Avalonia/MBTiles.Avalonia.csproj
+++ b/MBTiles/Avalonia/MBTiles.Avalonia.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/MBTiles/UWP/MBTiles.UWP.csproj b/MBTiles/UWP/MBTiles.UWP.csproj
index cf23e721..d3ba3b8d 100644
--- a/MBTiles/UWP/MBTiles.UWP.csproj
+++ b/MBTiles/UWP/MBTiles.UWP.csproj
@@ -11,7 +11,7 @@
MBTiles.UWP
en-US
UAP
- 10.0.22000.0
+ 10.0.22621.0
10.0.17763.0
14
512
diff --git a/MBTiles/WinUI/MBTiles.WinUI.csproj b/MBTiles/WinUI/MBTiles.WinUI.csproj
index 8cc4f389..ceb5497c 100644
--- a/MBTiles/WinUI/MBTiles.WinUI.csproj
+++ b/MBTiles/WinUI/MBTiles.WinUI.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/MapControl/Avalonia/MapControl.Avalonia.csproj b/MapControl/Avalonia/MapControl.Avalonia.csproj
index 34ccf0ac..3a5231b1 100644
--- a/MapControl/Avalonia/MapControl.Avalonia.csproj
+++ b/MapControl/Avalonia/MapControl.Avalonia.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/MapControl/Avalonia/TileImageLoader.Avalonia.cs b/MapControl/Avalonia/TileImageLoader.Avalonia.cs
index 37b35c39..225803b1 100644
--- a/MapControl/Avalonia/TileImageLoader.Avalonia.cs
+++ b/MapControl/Avalonia/TileImageLoader.Avalonia.cs
@@ -15,7 +15,6 @@ namespace MapControl
public static string DefaultCacheFolder =>
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl", "TileCache");
-
private static async Task LoadTileAsync(Tile tile, Func> loadImageFunc)
{
var image = await loadImageFunc().ConfigureAwait(false);
diff --git a/MapControl/Shared/ImageFileCache.cs b/MapControl/Shared/ImageFileCache.cs
index a1bde7b5..4c9aae2b 100644
--- a/MapControl/Shared/ImageFileCache.cs
+++ b/MapControl/Shared/ImageFileCache.cs
@@ -8,11 +8,11 @@ using Microsoft.Extensions.Options;
using System;
using System.Diagnostics;
using System.IO;
-using Path = System.IO.Path;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Path = System.IO.Path;
namespace MapControl.Caching
{
@@ -269,11 +269,13 @@ namespace MapControl.Caching
try
{
var hasExpired = false;
- var buffer = new byte[16];
using (var stream = file.OpenRead())
{
stream.Seek(-16, SeekOrigin.End);
+
+ var buffer = new byte[16];
+
hasExpired = stream.Read(buffer, 0, 16) == 16
&& GetExpirationTicks(buffer, out long expiration)
&& expiration <= DateTimeOffset.UtcNow.Ticks;
@@ -377,10 +379,11 @@ namespace MapControl.Caching
private static Task WriteAsync(Stream stream, byte[] bytes) => stream.WriteAsync(bytes, 0, bytes.Length);
- static partial void SetAccessControl(string path);
-#if !UWP && !AVALONIA
- static partial void SetAccessControl(string path)
+ private static void SetAccessControl(string path)
{
+#if AVALONIA
+ if (!OperatingSystem.IsWindows()) return;
+#endif
var fileInfo = new FileInfo(path);
var fileSecurity = fileInfo.GetAccessControl();
var fullControlRule = new System.Security.AccessControl.FileSystemAccessRule(
@@ -392,6 +395,5 @@ namespace MapControl.Caching
fileSecurity.AddAccessRule(fullControlRule);
fileInfo.SetAccessControl(fileSecurity);
}
-#endif
}
}
diff --git a/MapControl/UWP/ImageFileCache.UWP.cs b/MapControl/UWP/ImageFileCache.UWP.cs
new file mode 100644
index 00000000..b227df58
--- /dev/null
+++ b/MapControl/UWP/ImageFileCache.UWP.cs
@@ -0,0 +1,272 @@
+// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
+// Copyright © 2024 Clemens Fischer
+// Licensed under the Microsoft Public License (Ms-PL)
+
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.InteropServices.WindowsRuntime;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Windows.Storage;
+using Windows.Storage.Streams;
+using Buffer = Windows.Storage.Streams.Buffer;
+
+namespace MapControl.Caching
+{
+ ///
+ /// IDistributedCache implementation based on local image files.
+ ///
+ public partial class ImageFileCache : IDistributedCache
+ {
+ private static readonly byte[] expirationTag = Encoding.ASCII.GetBytes("EXPIRES:");
+
+ private readonly MemoryDistributedCache memoryCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()));
+ private readonly StorageFolder rootFolder;
+
+ public ImageFileCache(StorageFolder folder)
+ {
+ rootFolder = folder ?? throw new ArgumentException($"The {nameof(folder)} argument must not be null or empty.", nameof(folder));
+
+ Debug.WriteLine($"ImageFileCache: {rootFolder}");
+
+ _ = Task.Factory.StartNew(CleanAsync, TaskCreationOptions.LongRunning);
+ }
+
+ public byte[] Get(string key)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Set(string key, byte[] buffer, DistributedCacheEntryOptions options)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Remove(string key)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task RemoveAsync(string key, CancellationToken token = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Refresh(string key)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task RefreshAsync(string key, CancellationToken token = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ public async Task GetAsync(string key, CancellationToken token = default)
+ {
+ var buffer = await memoryCache.GetAsync(key, token).ConfigureAwait(false);
+
+ if (buffer == null)
+ {
+ try
+ {
+ var item = await rootFolder.TryGetItemAsync(key.Replace('/', '\\'));
+
+ if (item is StorageFile file)
+ {
+ buffer = (await FileIO.ReadBufferAsync(file)).ToArray();
+
+ if (CheckExpiration(ref buffer, out DistributedCacheEntryOptions options))
+ {
+ await memoryCache.SetAsync(key, buffer, options, token).ConfigureAwait(false);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ImageFileCache: Failed reading {key}: {ex.Message}");
+ }
+ }
+
+ return buffer;
+ }
+
+ public async Task SetAsync(string key, byte[] buffer, DistributedCacheEntryOptions options, CancellationToken token = default)
+ {
+ await memoryCache.SetAsync(key, buffer, options, token).ConfigureAwait(false);
+
+ if (buffer?.Length > 0)
+ {
+ try
+ {
+ var keyComponents = key.Split('/');
+ var folder = rootFolder;
+
+ for (int i = 0; i < keyComponents.Length - 1; i++)
+ {
+ folder = await folder.CreateFolderAsync(keyComponents[i], CreationCollisionOption.OpenIfExists);
+ }
+
+ var file = await folder.CreateFileAsync(keyComponents[keyComponents.Length - 1], CreationCollisionOption.OpenIfExists);
+
+ using (var stream = await file.OpenAsync(FileAccessMode.ReadWrite))
+ {
+ await stream.WriteAsync(buffer.AsBuffer());
+
+ if (GetExpirationBytes(options, out byte[] expiration))
+ {
+ await stream.WriteAsync(expiration.AsBuffer());
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ImageFileCache: Failed writing {key}: {ex.Message}");
+ }
+ }
+ }
+
+ public async Task CleanAsync()
+ {
+ var deletedFileCount = await CleanFolder(rootFolder);
+
+ if (deletedFileCount > 0)
+ {
+ Debug.WriteLine($"ImageFileCache: Deleted {deletedFileCount} expired files.");
+ }
+ }
+
+ private static async Task CleanFolder(StorageFolder folder)
+ {
+ var deletedFileCount = 0;
+
+ try
+ {
+ foreach (var f in await folder.GetFoldersAsync())
+ {
+ deletedFileCount += await CleanFolder(f);
+ }
+
+ foreach (var f in await folder.GetFilesAsync())
+ {
+ deletedFileCount += await CleanFile(f);
+ }
+
+ if ((await folder.GetItemsAsync()).Count == 0)
+ {
+ await folder.DeleteAsync();
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ImageFileCache: Failed cleaning {folder.Path}: {ex.Message}");
+ }
+
+ return deletedFileCount;
+ }
+
+ private static async Task CleanFile(StorageFile file)
+ {
+ var deletedFileCount = 0;
+ var size = (await file.GetBasicPropertiesAsync()).Size;
+
+ if (size > 16)
+ {
+ try
+ {
+ var hasExpired = false;
+
+ using (var stream = await file.OpenReadAsync())
+ {
+ stream.Seek(size - 16);
+
+ var buffer = await stream.ReadAsync(new Buffer(16), 16, InputStreamOptions.None);
+
+ hasExpired = buffer.Length == 16
+ && GetExpirationTicks(buffer.ToArray(), out long expiration)
+ && expiration <= DateTimeOffset.UtcNow.Ticks;
+ }
+
+ if (hasExpired)
+ {
+ await file.DeleteAsync();
+ deletedFileCount = 1;
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ImageFileCache: Failed cleaning {file.Path}: {ex.Message}");
+ }
+ }
+
+ return deletedFileCount;
+ }
+
+ private static bool CheckExpiration(ref byte[] buffer, out DistributedCacheEntryOptions options)
+ {
+ if (GetExpirationTicks(buffer, out long expiration))
+ {
+ if (expiration > DateTimeOffset.UtcNow.Ticks)
+ {
+ Array.Resize(ref buffer, buffer.Length - 16);
+
+ options = new DistributedCacheEntryOptions
+ {
+ AbsoluteExpiration = new DateTimeOffset(expiration, TimeSpan.Zero)
+ };
+
+ return true;
+ }
+
+ buffer = null; // buffer has expired
+ }
+
+ options = null;
+ return false;
+ }
+
+ private static bool GetExpirationTicks(byte[] buffer, out long expirationTicks)
+ {
+ if (buffer.Length >= 16 &&
+ expirationTag.SequenceEqual(buffer.Skip(buffer.Length - 16).Take(8)))
+ {
+ expirationTicks = BitConverter.ToInt64(buffer, buffer.Length - 8);
+ return true;
+ }
+
+ expirationTicks = 0;
+ return false;
+ }
+
+ private static bool GetExpirationBytes(DistributedCacheEntryOptions options, out byte[] expirationBytes)
+ {
+ long expirationTicks;
+
+ if (options.AbsoluteExpiration.HasValue)
+ {
+ expirationTicks = options.AbsoluteExpiration.Value.Ticks;
+ }
+ else if (options.AbsoluteExpirationRelativeToNow.HasValue)
+ {
+ expirationTicks = DateTimeOffset.UtcNow.Add(options.AbsoluteExpirationRelativeToNow.Value).Ticks;
+ }
+ else if (options.SlidingExpiration.HasValue)
+ {
+ expirationTicks = DateTimeOffset.UtcNow.Add(options.SlidingExpiration.Value).Ticks;
+ }
+ else
+ {
+ expirationBytes = null;
+ return false;
+ }
+
+ expirationBytes = expirationTag.Concat(BitConverter.GetBytes(expirationTicks)).ToArray();
+ return true;
+ }
+ }
+}
diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj
index 24bf291b..1a2f5e5b 100644
--- a/MapControl/UWP/MapControl.UWP.csproj
+++ b/MapControl/UWP/MapControl.UWP.csproj
@@ -11,7 +11,7 @@
MapControl.UWP
en-US
UAP
- 10.0.22000.0
+ 10.0.22621.0
10.0.17763.0
14
512
@@ -83,9 +83,6 @@
GroundOverlay.cs
-
- ImageFileCache.cs
-
ImageLoader.cs
@@ -275,6 +272,7 @@
Tile.WinUI.cs
+
diff --git a/MapControl/UWP/TileImageLoader.UWP.cs b/MapControl/UWP/TileImageLoader.UWP.cs
index c57b82ed..ee27b03d 100644
--- a/MapControl/UWP/TileImageLoader.UWP.cs
+++ b/MapControl/UWP/TileImageLoader.UWP.cs
@@ -15,8 +15,7 @@ namespace MapControl
///
/// Default folder where the Cache instance may save data.
///
- public static string DefaultCacheFolder => ApplicationData.Current.TemporaryFolder.Path;
-
+ public static StorageFolder DefaultCacheFolder => ApplicationData.Current.LocalCacheFolder;
private static async Task LoadTileAsync(Tile tile, Func> loadImageFunc)
{
diff --git a/MapControl/WPF/TileImageLoader.WPF.cs b/MapControl/WPF/TileImageLoader.WPF.cs
index 0bac2ee2..0aacd1a0 100644
--- a/MapControl/WPF/TileImageLoader.WPF.cs
+++ b/MapControl/WPF/TileImageLoader.WPF.cs
@@ -17,7 +17,6 @@ namespace MapControl
public static string DefaultCacheFolder =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl", "TileCache");
-
private static async Task LoadTileAsync(Tile tile, Func> loadImageFunc)
{
var image = await loadImageFunc().ConfigureAwait(false);
diff --git a/MapControl/WinUI/MapControl.WinUI.csproj b/MapControl/WinUI/MapControl.WinUI.csproj
index 8768f4fe..c4153771 100644
--- a/MapControl/WinUI/MapControl.WinUI.csproj
+++ b/MapControl/WinUI/MapControl.WinUI.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/MapControl/WinUI/TileImageLoader.WinUI.cs b/MapControl/WinUI/TileImageLoader.WinUI.cs
index 3109f0d7..135f0909 100644
--- a/MapControl/WinUI/TileImageLoader.WinUI.cs
+++ b/MapControl/WinUI/TileImageLoader.WinUI.cs
@@ -18,7 +18,6 @@ namespace MapControl
public static string DefaultCacheFolder =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl", "TileCache");
-
private static Task LoadTileAsync(Tile tile, Func> loadImageFunc)
{
var tcs = new TaskCompletionSource();
diff --git a/MapProjections/Avalonia/MapProjections.Avalonia.csproj b/MapProjections/Avalonia/MapProjections.Avalonia.csproj
index 70d83ccf..9c471e32 100644
--- a/MapProjections/Avalonia/MapProjections.Avalonia.csproj
+++ b/MapProjections/Avalonia/MapProjections.Avalonia.csproj
@@ -19,7 +19,7 @@
-
+
diff --git a/MapProjections/UWP/MapProjections.UWP.csproj b/MapProjections/UWP/MapProjections.UWP.csproj
index 872c94b7..8500d368 100644
--- a/MapProjections/UWP/MapProjections.UWP.csproj
+++ b/MapProjections/UWP/MapProjections.UWP.csproj
@@ -11,7 +11,7 @@
MapProjections.UWP
en-US
UAP
- 10.0.22000.0
+ 10.0.22621.0
10.0.17763.0
14
512
diff --git a/MapProjections/WinUI/MapProjections.WinUI.csproj b/MapProjections/WinUI/MapProjections.WinUI.csproj
index 35f202d1..3d41cafc 100644
--- a/MapProjections/WinUI/MapProjections.WinUI.csproj
+++ b/MapProjections/WinUI/MapProjections.WinUI.csproj
@@ -22,7 +22,7 @@
-
+
diff --git a/MapUiTools/Avalonia/MapUiTools.Avalonia.csproj b/MapUiTools/Avalonia/MapUiTools.Avalonia.csproj
index 62ea2731..0d5ba966 100644
--- a/MapUiTools/Avalonia/MapUiTools.Avalonia.csproj
+++ b/MapUiTools/Avalonia/MapUiTools.Avalonia.csproj
@@ -15,6 +15,6 @@
-
+
diff --git a/MapUiTools/UWP/MapUiTools.UWP.csproj b/MapUiTools/UWP/MapUiTools.UWP.csproj
index 1d0c5852..8cf295b6 100644
--- a/MapUiTools/UWP/MapUiTools.UWP.csproj
+++ b/MapUiTools/UWP/MapUiTools.UWP.csproj
@@ -11,7 +11,7 @@
MapUiTools.UWP
en-US
UAP
- 10.0.22000.0
+ 10.0.22621.0
10.0.17763.0
14
512
diff --git a/MapUiTools/WinUI/MapUiTools.WinUI.csproj b/MapUiTools/WinUI/MapUiTools.WinUI.csproj
index 56034d7f..f22e058d 100644
--- a/MapUiTools/WinUI/MapUiTools.WinUI.csproj
+++ b/MapUiTools/WinUI/MapUiTools.WinUI.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/SampleApps/AvaloniaApp/AvaloniaApp.csproj b/SampleApps/AvaloniaApp/AvaloniaApp.csproj
index 871c2479..04ba67be 100644
--- a/SampleApps/AvaloniaApp/AvaloniaApp.csproj
+++ b/SampleApps/AvaloniaApp/AvaloniaApp.csproj
@@ -32,11 +32,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/SampleApps/UniversalApp/UniversalApp.csproj b/SampleApps/UniversalApp/UniversalApp.csproj
index c3440fe5..4d2e26c9 100644
--- a/SampleApps/UniversalApp/UniversalApp.csproj
+++ b/SampleApps/UniversalApp/UniversalApp.csproj
@@ -11,7 +11,7 @@
UniversalApp
en-US
UAP
- 10.0.22000.0
+ 10.0.22621.0
10.0.17763.0
14
true
diff --git a/SampleApps/WinUiApp/WinUiApp.csproj b/SampleApps/WinUiApp/WinUiApp.csproj
index 01983a6a..0ba425d2 100644
--- a/SampleApps/WinUiApp/WinUiApp.csproj
+++ b/SampleApps/WinUiApp/WinUiApp.csproj
@@ -37,7 +37,7 @@
-
+