mirror of
https://github.com/ClemensFischer/XAML-Map-Control.git
synced 2026-04-05 14:37:01 +00:00
Replaced local file caching by persistent ObjectCache based on FileDb.
Removed IsCached and ImageType properties from TileLayer.
This commit is contained in:
parent
38e6c23114
commit
9652fc2f56
13 changed files with 2297 additions and 148 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue