Replaced local file caching by persistent ObjectCache based on FileDb.

Removed IsCached and ImageType properties from TileLayer.
This commit is contained in:
ClemensF 2012-07-03 18:03:56 +02:00
parent 38e6c23114
commit 9652fc2f56
13 changed files with 2297 additions and 148 deletions

View file

@ -3,7 +3,6 @@
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
@ -49,10 +48,5 @@ namespace MapControl
Brush.ImageSource = value;
}
}
public override string ToString()
{
return string.Format("{0}.{1}.{2}", ZoomLevel, X, Y);
}
}
}

View file

@ -4,11 +4,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Caching;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
@ -16,28 +18,70 @@ using System.Windows.Threading;
namespace MapControl
{
/// <summary>
/// Loads map tiles by their URIs and optionally caches their image files in a folder
/// defined by the static TileCacheFolder property.
/// Loads map tile images by their URIs and optionally caches the images in an ObjectCache.
/// </summary>
public class TileImageLoader : DispatcherObject
{
public static string TileCacheFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl Cache");
public static TimeSpan TileCacheExpiryAge = TimeSpan.FromDays(1d);
[Serializable]
private class CachedImage
{
public readonly DateTime CreationTime = DateTime.UtcNow;
public readonly byte[] ImageBuffer;
public CachedImage(byte[] imageBuffer)
{
ImageBuffer = imageBuffer;
}
}
private readonly TileLayer tileLayer;
private readonly Queue<Tile> pendingTiles = new Queue<Tile>();
private readonly HashSet<HttpWebRequest> currentRequests = new HashSet<HttpWebRequest>();
private int numDownloads;
/// <summary>
/// The ObjectCache used to cache tile images.
/// The default is System.Runtime.Caching.MemoryCache.Default.
/// </summary>
public static ObjectCache Cache { get; set; }
/// <summary>
/// The time interval after which cached images expire. The default value is 30 days.
/// When an image is not retrieved from the cache during this interval it is considered
/// as expired and will be removed from the cache. If an image is retrieved from the cache
/// and the CacheUpdateAge time interval has expired, the image is downloaded again and
/// rewritten to the cache with new expiration time.
/// </summary>
public static TimeSpan CacheExpiration { get; set; }
/// <summary>
/// The time interval after which a cached image is updated and rewritten to the cache.
/// The default value is one day. This time interval should be shorter than the value of
/// the CacheExpiration property.
/// </summary>
public static TimeSpan CacheUpdateAge { get; set; }
static TileImageLoader()
{
Cache = MemoryCache.Default;
CacheExpiration = TimeSpan.FromDays(30d);
CacheUpdateAge = TimeSpan.FromDays(1d);
Application.Current.Exit += (o, e) =>
{
IDisposable disposableCache = Cache as IDisposable;
if (disposableCache != null)
{
disposableCache.Dispose();
}
};
}
public TileImageLoader(TileLayer tileLayer)
{
this.tileLayer = tileLayer;
}
private bool IsCached
{
get { return tileLayer.IsCached && !string.IsNullOrEmpty(TileCacheFolder); }
}
internal void StartDownloadTiles(ICollection<Tile> tiles)
{
ThreadPool.QueueUserWorkItem(StartDownloadTilesAsync, new List<Tile>(tiles.Where(t => t.Image == null && t.Uri == null)));
@ -49,46 +93,54 @@ namespace MapControl
{
pendingTiles.Clear();
}
lock (currentRequests)
{
foreach (HttpWebRequest request in currentRequests)
{
request.Abort();
}
}
}
private void StartDownloadTilesAsync(object newTilesList)
{
List<Tile> newTiles = (List<Tile>)newTilesList;
List<Tile> expiredTiles = new List<Tile>(newTiles.Count);
lock (pendingTiles)
{
newTiles.ForEach(tile =>
if (Cache == null)
{
ImageSource image = GetMemoryCachedImage(tile);
newTiles.ForEach(tile => pendingTiles.Enqueue(tile));
}
else
{
List<Tile> outdatedTiles = new List<Tile>(newTiles.Count);
if (image == null && IsCached)
newTiles.ForEach(tile =>
{
bool fileCacheExpired;
image = GetFileCachedImage(tile, out fileCacheExpired);
string key = CacheKey(tile);
CachedImage cachedImage = Cache.Get(key) as CachedImage;
if (image != null)
if (cachedImage == null)
{
SetMemoryCachedImage(tile, image);
if (fileCacheExpired)
{
expiredTiles.Add(tile); // enqueue later
}
pendingTiles.Enqueue(tile);
}
}
else if (!CreateTileImage(tile, cachedImage.ImageBuffer))
{
// got garbage from cache
Cache.Remove(key);
pendingTiles.Enqueue(tile);
}
else if (cachedImage.CreationTime + CacheUpdateAge < DateTime.UtcNow)
{
// update cached image
outdatedTiles.Add(tile);
}
});
if (image != null)
{
Dispatcher.BeginInvoke((Action)(() => tile.Image = image));
}
else
{
pendingTiles.Enqueue(tile);
}
});
expiredTiles.ForEach(tile => pendingTiles.Enqueue(tile));
outdatedTiles.ForEach(tile => pendingTiles.Enqueue(tile));
}
DownloadNextTiles(null);
}
@ -109,13 +161,13 @@ namespace MapControl
private void DownloadTileAsync(object t)
{
Tile tile = (Tile)t;
ImageSource image = DownloadImage(tile);
byte[] imageBuffer = DownloadImage(tile);
if (image != null)
if (imageBuffer != null &&
CreateTileImage(tile, imageBuffer) &&
Cache != null)
{
SetMemoryCachedImage(tile, image);
Dispatcher.BeginInvoke((Action)(() => tile.Image = image));
Cache.Set(CacheKey(tile), new CachedImage(imageBuffer), new CacheItemPolicy { SlidingExpiration = CacheExpiration });
}
lock (pendingTiles)
@ -125,98 +177,38 @@ namespace MapControl
}
}
private string MemoryCacheKey(Tile tile)
private string CacheKey(Tile tile)
{
return string.Format("{0}/{1}/{2}/{3}", tileLayer.Name, tile.ZoomLevel, tile.XIndex, tile.Y);
return string.Format("{0}-{1}-{2}-{3}", tileLayer.Name, tile.ZoomLevel, tile.XIndex, tile.Y);
}
private string CacheFilePath(Tile tile)
private byte[] DownloadImage(Tile tile)
{
return string.Format("{0}.{1}",
Path.Combine(TileCacheFolder, tileLayer.Name, tile.ZoomLevel.ToString(), tile.XIndex.ToString(), tile.Y.ToString()),
tileLayer.ImageType);
}
private ImageSource GetMemoryCachedImage(Tile tile)
{
string key = MemoryCacheKey(tile);
ImageSource image = MemoryCache.Default.Get(key) as ImageSource;
if (image != null)
{
TraceInformation("{0} - Memory Cached", key);
}
return image;
}
private void SetMemoryCachedImage(Tile tile, ImageSource image)
{
MemoryCache.Default.Set(MemoryCacheKey(tile), image,
new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(10d) });
}
private ImageSource GetFileCachedImage(Tile tile, out bool expired)
{
string path = CacheFilePath(tile);
ImageSource image = null;
expired = false;
if (File.Exists(path))
{
try
{
using (Stream fileStream = File.OpenRead(path))
{
image = BitmapFrame.Create(fileStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
expired = File.GetLastWriteTime(path) + TileCacheExpiryAge <= DateTime.Now;
TraceInformation(expired ? "{0} - File Cache Expired" : "{0} - File Cached", path);
}
catch (Exception exc)
{
TraceWarning("{0} - {1}", path, exc.Message);
File.Delete(path);
}
}
return image;
}
private ImageSource DownloadImage(Tile tile)
{
ImageSource image = null;
HttpWebRequest request = null;
byte[] buffer = null;
try
{
TraceInformation("{0} - Requesting", tile.Uri);
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(tile.Uri);
webRequest.UserAgent = typeof(TileImageLoader).ToString();
webRequest.KeepAlive = true;
request = (HttpWebRequest)WebRequest.Create(tile.Uri);
request.UserAgent = typeof(TileImageLoader).ToString();
request.KeepAlive = true;
using (HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse())
lock (currentRequests)
{
currentRequests.Add(request);
}
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
using (Stream responseStream = response.GetResponseStream())
{
using (Stream memoryStream = new MemoryStream((int)response.ContentLength))
buffer = new byte[(int)response.ContentLength];
using (MemoryStream memoryStream = new MemoryStream(buffer))
{
responseStream.CopyTo(memoryStream);
memoryStream.Position = 0;
image = BitmapFrame.Create(memoryStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
if (IsCached)
{
string path = CacheFilePath(tile);
Directory.CreateDirectory(Path.GetDirectoryName(path));
using (Stream fileStream = File.OpenWrite(path))
{
memoryStream.Position = 0;
memoryStream.CopyTo(fileStream);
}
}
}
}
}
@ -225,10 +217,16 @@ namespace MapControl
}
catch (WebException exc)
{
buffer = null;
if (exc.Status == WebExceptionStatus.ProtocolError)
{
TraceInformation("{0} - {1}", tile.Uri, ((HttpWebResponse)exc.Response).StatusCode);
}
else if (exc.Status == WebExceptionStatus.RequestCanceled)
{
TraceInformation("{0} - {1}", tile.Uri, exc.Status);
}
else
{
TraceWarning("{0} - {1}", tile.Uri, exc.Status);
@ -236,20 +234,56 @@ namespace MapControl
}
catch (Exception exc)
{
buffer = null;
TraceWarning("{0} - {1}", tile.Uri, exc.Message);
}
return image;
if (request != null)
{
lock (currentRequests)
{
currentRequests.Remove(request);
}
}
return buffer;
}
private bool CreateTileImage(Tile tile, byte[] buffer)
{
try
{
BitmapImage bitmap = new BitmapImage();
using (Stream stream = new MemoryStream(buffer))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}
Dispatcher.BeginInvoke((Action)(() => tile.Image = bitmap));
}
catch (Exception exc)
{
TraceWarning("Creating tile image failed: {0}", exc.Message);
return false;
}
return true;
}
private static void TraceWarning(string format, params object[] args)
{
System.Diagnostics.Trace.TraceWarning("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args));
Trace.TraceWarning("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args));
}
private static void TraceInformation(string format, params object[] args)
{
//System.Diagnostics.Trace.TraceInformation("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args));
Trace.TraceInformation("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args));
}
}
}

View file

@ -12,8 +12,7 @@ using System.Windows.Media;
namespace MapControl
{
/// <summary>
/// Fills a rectangular area with map tiles from a TileSource. If the IsCached property is true,
/// map tiles are cached in a folder defined by the TileImageLoader.TileCacheFolder property.
/// Fills a rectangular area with map tiles from a TileSource.
/// </summary>
[ContentProperty("TileSource")]
public class TileLayer : DrawingVisual
@ -30,19 +29,16 @@ namespace MapControl
VisualEdgeMode = EdgeMode.Aliased;
VisualTransform = new MatrixTransform();
Name = string.Empty;
ImageType = "png";
MinZoomLevel = 1;
MaxZoomLevel = 18;
MaxDownloads = 8;
}
public string Name { get; set; }
public string ImageType { get; set; }
public TileSource TileSource { get; set; }
public int MinZoomLevel { get; set; }
public int MaxZoomLevel { get; set; }
public int MaxDownloads { get; set; }
public bool IsCached { get; set; }
public bool HasDarkBackground { get; set; }
public string Description
@ -139,7 +135,7 @@ namespace MapControl
drawingContext.DrawRectangle(tile.Brush, null, tileRect);
//if (tile.ZoomLevel == zoomLevel)
// drawingContext.DrawText(new FormattedText(tile.ToString(), System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, new Typeface("Segoe UI"), 14, Brushes.Black), tileRect.TopLeft);
// drawingContext.DrawText(new FormattedText(string.Format("{0}-{1}-{2}", tile.ZoomLevel, tile.X, tile.Y), System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, new Typeface("Segoe UI"), 14, Brushes.Black), tileRect.TopLeft);
});
}
}