Version 4.12.2 Improved TileImageLoader.Cache

This commit is contained in:
ClemensF 2019-07-18 21:09:54 +02:00
parent a25cc91c2f
commit 85287118a5
8 changed files with 139 additions and 116 deletions

View file

@ -53,6 +53,12 @@
</None> </None>
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\MapControl\WPF\MapControl.WPF.csproj">
<Project>{a204a102-c745-4d65-aec8-7b96faedef2d}</Project>
<Name>MapControl.WPF</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

View file

@ -149,11 +149,15 @@ namespace MapControl.Caching
{ {
try try
{ {
var record = fileDb.GetRecordByKey(key, new string[] { valueField }, false); var record = fileDb.GetRecordByKey(key, new string[] { valueField, expiresField }, false);
if (record != null) if (record != null)
{ {
return record[0]; return new ImageCacheItem
{
Buffer = (byte[])record[0],
Expiration = (DateTime)record[1]
};
} }
} }
catch (Exception ex) catch (Exception ex)
@ -184,38 +188,21 @@ namespace MapControl.Caching
throw new ArgumentNullException("The parameter key must not be null."); throw new ArgumentNullException("The parameter key must not be null.");
} }
if (value == null)
{
throw new ArgumentNullException("The parameter value must not be null.");
}
if (policy == null)
{
throw new ArgumentNullException("The parameter policy must not be null.");
}
if (regionName != null) if (regionName != null)
{ {
throw new NotSupportedException("The parameter regionName must be null."); throw new NotSupportedException("The parameter regionName must be null.");
} }
if (fileDb.IsOpen) var imageCacheItem = value as ImageCacheItem;
if (imageCacheItem == null || imageCacheItem.Buffer == null || imageCacheItem.Buffer.Length == 0)
{ {
var expiration = DateTime.MaxValue; throw new NotSupportedException("The parameter value must be an ImageCacheItem with a non-empty Buffer.");
}
if (policy.AbsoluteExpiration != InfiniteAbsoluteExpiration) if (fileDb.IsOpen && !AddOrUpdateRecord(key, imageCacheItem) && RepairDatabase())
{ {
expiration = policy.AbsoluteExpiration.DateTime; AddOrUpdateRecord(key, imageCacheItem);
}
else if (policy.SlidingExpiration != NoSlidingExpiration)
{
expiration = DateTime.UtcNow + policy.SlidingExpiration;
}
if (!AddOrUpdateRecord(key, value, expiration) && RepairDatabase())
{
AddOrUpdateRecord(key, value, expiration);
}
} }
} }
@ -354,11 +341,11 @@ namespace MapControl.Caching
return false; return false;
} }
private bool AddOrUpdateRecord(string key, object value, DateTime expiration) private bool AddOrUpdateRecord(string key, ImageCacheItem imageCacheItem)
{ {
var fieldValues = new FieldValues(3); var fieldValues = new FieldValues(3);
fieldValues.Add(valueField, value); fieldValues.Add(valueField, imageCacheItem.Buffer);
fieldValues.Add(expiresField, expiration); fieldValues.Add(expiresField, imageCacheItem.Expiration);
bool recordExists; bool recordExists;
@ -398,7 +385,7 @@ namespace MapControl.Caching
} }
} }
//Debug.WriteLine("FileDbCache: Writing \"{0}\", Expires {1}", key, expiration.ToLocalTime()); //Debug.WriteLine("FileDbCache: Writing \"{0}\", Expires {1}", key, imageCacheItem.Expiration.ToLocalTime());
return true; return true;
} }
} }

View file

@ -20,12 +20,12 @@ namespace MapControl.MBTiles
{ {
private readonly SQLiteConnection connection; private readonly SQLiteConnection connection;
public MBTileData(string file) private MBTileData(string file)
{ {
connection = new SQLiteConnection("Data Source=" + Path.GetFullPath(file)); connection = new SQLiteConnection("Data Source=" + Path.GetFullPath(file));
} }
public async Task OpenAsync() private async Task OpenAsync()
{ {
await connection.OpenAsync(); await connection.OpenAsync();
@ -40,6 +40,15 @@ namespace MapControl.MBTiles
} }
} }
public static async Task<MBTileData> CreateAsync(string file)
{
var tileData = new MBTileData(file);
await tileData.OpenAsync();
return tileData;
}
public void Close() public void Close()
{ {
connection.Close(); connection.Close();
@ -50,9 +59,9 @@ namespace MapControl.MBTiles
connection.Dispose(); connection.Dispose();
} }
public async Task<IDictionary<string, string>> ReadMetadataAsync() public async Task<IDictionary<string, string>> ReadMetaDataAsync()
{ {
var metadata = new Dictionary<string, string>(); var metaData = new Dictionary<string, string>();
try try
{ {
@ -62,7 +71,7 @@ namespace MapControl.MBTiles
while (await reader.ReadAsync()) while (await reader.ReadAsync())
{ {
metadata[(string)reader["name"]] = (string)reader["value"]; metaData[(string)reader["name"]] = (string)reader["value"];
} }
} }
} }
@ -71,16 +80,16 @@ namespace MapControl.MBTiles
Debug.WriteLine("MBTileData: " + ex.Message); Debug.WriteLine("MBTileData: " + ex.Message);
} }
return metadata; return metaData;
} }
public async Task WriteMetadataAsync(IDictionary<string, string> metadata) public async Task WriteMetaDataAsync(IDictionary<string, string> metaData)
{ {
try try
{ {
using (var command = new SQLiteCommand("insert or replace into metadata (name, value) values (@n, @v)", connection)) using (var command = new SQLiteCommand("insert or replace into metadata (name, value) values (@n, @v)", connection))
{ {
foreach (var keyValue in metadata) foreach (var keyValue in metaData)
{ {
command.Parameters.AddWithValue("@n", keyValue.Key); command.Parameters.AddWithValue("@n", keyValue.Key);
command.Parameters.AddWithValue("@v", keyValue.Value); command.Parameters.AddWithValue("@v", keyValue.Value);

View file

@ -72,9 +72,7 @@ namespace MapControl.MBTiles
if (file != null) if (file != null)
{ {
mbTileSource = new MBTileSource(file); mbTileSource = await MBTileSource.CreateAsync(file);
await mbTileSource.Initialize();
if (mbTileSource.Name != null) if (mbTileSource.Name != null)
{ {

View file

@ -3,6 +3,7 @@
// Licensed under the Microsoft Public License (Ms-PL) // Licensed under the Microsoft Public License (Ms-PL)
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
#if WINDOWS_UWP #if WINDOWS_UWP
using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media;
@ -16,47 +17,45 @@ namespace MapControl.MBTiles
{ {
private readonly MBTileData tileData; private readonly MBTileData tileData;
public MBTileSource(string file) public string Name { get; }
public string Description { get; }
public int? MinZoom { get; }
public int? MaxZoom { get; }
private MBTileSource(MBTileData tileData, IDictionary<string, string> metaData)
{ {
tileData = new MBTileData(file); this.tileData = tileData;
}
public string Name { get; private set; }
public string Description { get; private set; }
public int? MinZoom { get; private set; }
public int? MaxZoom { get; private set; }
public async Task Initialize()
{
await tileData.OpenAsync();
var metadata = await tileData.ReadMetadataAsync();
string s; string s;
int minZoom; int minZoom;
int maxZoom; int maxZoom;
Name = (metadata.TryGetValue("name", out s)) ? s : null; if (metaData.TryGetValue("name", out s))
{
Name = s;
}
Description = (metadata.TryGetValue("description", out s)) ? s : null; if (metaData.TryGetValue("description", out s))
{
Description = s;
}
if (metadata.TryGetValue("minzoom", out s) && int.TryParse(s, out minZoom)) if (metaData.TryGetValue("minzoom", out s) && int.TryParse(s, out minZoom))
{ {
MinZoom = minZoom; MinZoom = minZoom;
} }
else
{
MinZoom = null;
}
if (metadata.TryGetValue("maxzoom", out s) && int.TryParse(s, out maxZoom)) if (metaData.TryGetValue("maxzoom", out s) && int.TryParse(s, out maxZoom))
{ {
MaxZoom = maxZoom; MaxZoom = maxZoom;
} }
else }
{
MaxZoom = null; public static async Task<MBTileSource> CreateAsync(string file)
} {
var tileData = await MBTileData.CreateAsync(file);
return new MBTileSource(tileData, await tileData.ReadMetaDataAsync());
} }
public void Dispose() public void Dispose()

View file

@ -33,7 +33,7 @@ namespace MapControl.Caching
try try
{ {
path = Path.Combine(key.Split('\\', '/', ':', ';')); path = Path.Combine(GetPathElements(key));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -69,18 +69,18 @@ namespace MapControl.Caching
public async Task SetAsync(string key, IBuffer buffer, DateTime expiration) public async Task SetAsync(string key, IBuffer buffer, DateTime expiration)
{ {
var paths = key.Split('\\', '/', ',', ':', ';'); var folders = GetPathElements(key);
try try
{ {
var folder = rootFolder; var folder = rootFolder;
for (int i = 0; i < paths.Length - 1; i++) for (int i = 0; i < folders.Length - 1; i++)
{ {
folder = await folder.CreateFolderAsync(paths[i], CreationCollisionOption.OpenIfExists); folder = await folder.CreateFolderAsync(folders[i], CreationCollisionOption.OpenIfExists);
} }
var file = await folder.CreateFileAsync(paths[paths.Length - 1], CreationCollisionOption.ReplaceExisting); var file = await folder.CreateFileAsync(folders[folders.Length - 1], CreationCollisionOption.ReplaceExisting);
//Debug.WriteLine("ImageFileCache: Writing {0}, Expires {1}", file.Path, expiration.ToLocalTime()); //Debug.WriteLine("ImageFileCache: Writing {0}, Expires {1}", file.Path, expiration.ToLocalTime());
await FileIO.WriteBufferAsync(file, buffer); await FileIO.WriteBufferAsync(file, buffer);
@ -88,12 +88,18 @@ namespace MapControl.Caching
// Store expiration date in ImageProperties.DateTaken // Store expiration date in ImageProperties.DateTaken
var properties = await file.Properties.GetImagePropertiesAsync(); var properties = await file.Properties.GetImagePropertiesAsync();
properties.DateTaken = expiration; properties.DateTaken = expiration;
await properties.SavePropertiesAsync(); await properties.SavePropertiesAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.WriteLine("ImageFileCache: Writing {0}\\{1}: {2}", rootFolder.Path, string.Join("\\", paths), ex.Message); Debug.WriteLine("ImageFileCache: Writing {0}: {1}", Path.Combine(rootFolder.Path, Path.Combine(folders)), ex.Message);
} }
} }
private string[] GetPathElements(string key)
{
return key.Split('\\', '/', ',', ':', ';');
}
} }
} }

View file

@ -10,12 +10,13 @@ using System.Linq;
using System.Runtime.Caching; using System.Runtime.Caching;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.Security.Principal; using System.Security.Principal;
using System.Text;
namespace MapControl.Caching namespace MapControl.Caching
{ {
/// <summary> /// <summary>
/// ObjectCache implementation based on local image files. /// ObjectCache implementation based on local image files.
/// The only valid data type for cached values is byte[]. /// The only valid data type for cached values is MapControl.ImageCacheItem.
/// </summary> /// </summary>
public class ImageFileCache : ObjectCache public class ImageFileCache : ObjectCache
{ {
@ -96,9 +97,9 @@ namespace MapControl.Caching
throw new NotSupportedException("The parameter regionName must be null."); throw new NotSupportedException("The parameter regionName must be null.");
} }
var buffer = memoryCache.Get(key) as byte[]; var imageCacheItem = memoryCache.Get(key) as ImageCacheItem;
if (buffer == null) if (imageCacheItem == null)
{ {
var path = FindFile(key); var path = FindFile(key);
@ -106,10 +107,24 @@ namespace MapControl.Caching
{ {
try try
{ {
//Debug.WriteLine("ImageFileCache: Reading " + path); var buffer = File.ReadAllBytes(path);
var expiration = DateTime.MinValue;
buffer = File.ReadAllBytes(path); if (buffer.Length > 16 && Encoding.ASCII.GetString(buffer, buffer.Length - 16, 8) == "EXPIRES:")
memoryCache.Set(key, buffer, new CacheItemPolicy()); {
expiration = new DateTime(BitConverter.ToInt64(buffer, buffer.Length - 8), DateTimeKind.Utc);
Array.Resize(ref buffer, buffer.Length - 16);
}
imageCacheItem = new ImageCacheItem
{
Buffer = buffer,
Expiration = expiration
};
memoryCache.Set(key, imageCacheItem, new CacheItemPolicy { AbsoluteExpiration = expiration });
//Debug.WriteLine("ImageFileCache: Reading {0}, Expires {1}", path, imageCacheItem.Expiration.ToLocalTime());
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -118,7 +133,7 @@ namespace MapControl.Caching
} }
} }
return buffer; return imageCacheItem;
} }
public override CacheItem GetCacheItem(string key, string regionName = null) public override CacheItem GetCacheItem(string key, string regionName = null)
@ -145,14 +160,14 @@ namespace MapControl.Caching
throw new NotSupportedException("The parameter regionName must be null."); throw new NotSupportedException("The parameter regionName must be null.");
} }
var buffer = value as byte[]; var imageCacheItem = value as ImageCacheItem;
if (buffer == null || buffer.Length == 0) if (imageCacheItem == null || imageCacheItem.Buffer == null || imageCacheItem.Buffer.Length == 0)
{ {
throw new NotSupportedException("The parameter value must be a non-empty byte array."); throw new NotSupportedException("The parameter value must be an ImageCacheItem with a non-empty Buffer.");
} }
memoryCache.Set(key, buffer, policy); memoryCache.Set(key, imageCacheItem, policy);
var path = GetPath(key); var path = GetPath(key);
@ -160,10 +175,16 @@ namespace MapControl.Caching
{ {
try try
{ {
//Debug.WriteLine("ImageFileCache: Writing {0}, Expires {1}", path, policy.AbsoluteExpiration.DateTime.ToLocalTime()); //Debug.WriteLine("ImageFileCache: Writing {0}, Expires {1}", path, imageCacheItem.Expiration.ToLocalTime());
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllBytes(path, buffer);
using (var stream = File.Create(path))
{
stream.Write(imageCacheItem.Buffer, 0, imageCacheItem.Buffer.Length);
stream.Write(Encoding.ASCII.GetBytes("EXPIRES:"), 0, 8);
stream.Write(BitConverter.GetBytes(imageCacheItem.Expiration.Ticks), 0, 8);
}
var fileSecurity = File.GetAccessControl(path); var fileSecurity = File.GetAccessControl(path);
fileSecurity.AddAccessRule(fullControlRule); fileSecurity.AddAccessRule(fullControlRule);

View file

@ -5,17 +5,21 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.Caching; using System.Runtime.Caching;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media; using System.Windows.Media;
namespace MapControl namespace MapControl
{ {
public class ImageCacheItem
{
public byte[] Buffer { get; set; }
public DateTime Expiration { get; set; }
}
public partial class TileImageLoader public partial class TileImageLoader
{ {
/// <summary> /// <summary>
/// Default folder path where an ObjectCache instance may save cached data, /// Default folder path where an ObjectCache instance may save cached data, i.e. C:\ProgramData\MapControl\TileCache
/// i.e. C:\ProgramData\MapControl\TileCache
/// </summary> /// </summary>
public static string DefaultCacheFolder public static string DefaultCacheFolder
{ {
@ -23,17 +27,18 @@ namespace MapControl
} }
/// <summary> /// <summary>
/// The ObjectCache used to cache tile images. The default is MemoryCache.Default. /// An ObjectCache instance used to cache tile image data (i.e. ImageCacheItem objects).
/// The default ObjectCache value is MemoryCache.Default.
/// </summary> /// </summary>
public static ObjectCache Cache { get; set; } = MemoryCache.Default; public static ObjectCache Cache { get; set; } = MemoryCache.Default;
private static async Task LoadCachedTileImageAsync(Tile tile, Uri uri, string cacheKey) private static async Task LoadCachedTileImageAsync(Tile tile, Uri uri, string cacheKey)
{ {
DateTime expiration; var cacheItem = await GetCacheAsync(cacheKey).ConfigureAwait(false);
var buffer = GetCachedImage(cacheKey, out expiration); var buffer = cacheItem?.Buffer;
if (buffer == null || expiration < DateTime.UtcNow) if (buffer == null || cacheItem.Expiration < DateTime.UtcNow)
{ {
var response = await ImageLoader.GetHttpResponseAsync(uri, false).ConfigureAwait(false); var response = await ImageLoader.GetHttpResponseAsync(uri, false).ConfigureAwait(false);
@ -43,7 +48,7 @@ namespace MapControl
if (buffer != null) // tile image available if (buffer != null) // tile image available
{ {
await SetCachedImage(cacheKey, buffer, GetExpiration(response.MaxAge)).ConfigureAwait(false); await SetCacheAsync(cacheKey, buffer, GetExpiration(response.MaxAge)).ConfigureAwait(false);
} }
} }
} }
@ -69,33 +74,25 @@ namespace MapControl
tile.Image.Dispatcher.InvokeAsync(() => tile.SetImage(image)); tile.Image.Dispatcher.InvokeAsync(() => tile.SetImage(image));
} }
private static byte[] GetCachedImage(string cacheKey, out DateTime expiration) private static Task<ImageCacheItem> GetCacheAsync(string cacheKey)
{ {
var buffer = Cache.Get(cacheKey) as byte[]; return Task.Run(() => Cache.Get(cacheKey) as ImageCacheItem);
if (buffer != null && buffer.Length >= 16 &&
Encoding.ASCII.GetString(buffer, buffer.Length - 16, 8) == "EXPIRES:")
{
expiration = new DateTime(BitConverter.ToInt64(buffer, buffer.Length - 8), DateTimeKind.Utc);
}
else
{
expiration = DateTime.MinValue;
}
return buffer;
} }
private static async Task SetCachedImage(string cacheKey, byte[] buffer, DateTime expiration) private static Task SetCacheAsync(string cacheKey, byte[] buffer, DateTime expiration)
{ {
using (var stream = new MemoryStream(buffer.Length + 16)) var imageCacheItem = new ImageCacheItem
{ {
await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false); Buffer = buffer,
await stream.WriteAsync(Encoding.ASCII.GetBytes("EXPIRES:"), 0, 8).ConfigureAwait(false); Expiration = expiration
await stream.WriteAsync(BitConverter.GetBytes(expiration.Ticks), 0, 8).ConfigureAwait(false); };
Cache.Set(cacheKey, stream.ToArray(), new CacheItemPolicy { AbsoluteExpiration = expiration }); var cacheItemPolicy = new CacheItemPolicy
} {
AbsoluteExpiration = expiration
};
return Task.Run(() => Cache.Set(cacheKey, imageCacheItem, cacheItemPolicy));
} }
} }
} }