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

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -0,0 +1,5 @@
FileDbCache uses FileDb by Brett Goodman (aka EzTools), a simple No-SQL database for .NET.
FileDb is Open Source on Google Code Hosting at http://code.google.com/p/filedb-database/
and licensed under the Apache License 2.0, http://www.apache.org/licenses/LICENSE-2.0.
See the product homepage at http://www.eztools-software.com/tools/filedb/.
Download FileDb from http://www.eztools-software.com/downloads/filedb.exe.

View file

@ -0,0 +1,444 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.Caching;
using System.Runtime.Serialization.Formatters.Binary;
using FileDbNs;
namespace Caching
{
/// <summary>
/// ObjectCache implementation based on EzTools FileDb - http://www.eztools-software.com/tools/filedb/.
/// </summary>
public class FileDbCache : ObjectCache, IDisposable
{
private const string keyField = "Key";
private const string valueField = "Value";
private const string expiresField = "Expires";
private readonly BinaryFormatter formatter = new BinaryFormatter();
private readonly FileDb fileDb = new FileDb();
private readonly string name;
private readonly string path;
public FileDbCache(string name, string path)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("The parameter path must not be null or empty.");
}
if (string.IsNullOrEmpty(Path.GetExtension(path)))
{
path += ".fdb";
}
this.name = name;
this.path = path;
try
{
fileDb.Open(path, false);
}
catch
{
CreateDatebase();
}
Trace.TraceInformation("FileDbCache created with {0} cached items", fileDb.NumRecords);
}
public bool AutoFlush
{
get { return fileDb.AutoFlush; }
set { fileDb.AutoFlush = value; }
}
public int AutoCleanThreshold
{
get { return fileDb.AutoCleanThreshold; }
set { fileDb.AutoCleanThreshold = value; }
}
public override string Name
{
get { return name; }
}
public override DefaultCacheCapabilities DefaultCacheCapabilities
{
get { return DefaultCacheCapabilities.InMemoryProvider | DefaultCacheCapabilities.AbsoluteExpirations | DefaultCacheCapabilities.SlidingExpirations; }
}
public override object this[string key]
{
get { return Get(key); }
set { Set(key, value, null); }
}
protected override IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
throw new NotSupportedException("FileDbCache does not support the ability to enumerate items.");
}
public override CacheEntryChangeMonitor CreateCacheEntryChangeMonitor(IEnumerable<string> keys, string regionName = null)
{
throw new NotSupportedException("FileDbCache does not support the ability to create change monitors.");
}
public override long GetCount(string regionName = null)
{
if (regionName != null)
{
throw new NotSupportedException("The parameter regionName must be null.");
}
long count = 0;
try
{
count = fileDb.NumRecords;
}
catch
{
if (CheckReindex())
{
try
{
count = fileDb.NumRecords;
}
catch
{
CreateDatebase();
}
}
}
return count;
}
public override bool Contains(string key, string regionName = null)
{
if (regionName != null)
{
throw new NotSupportedException("The parameter regionName must be null.");
}
if (key == null)
{
throw new ArgumentNullException("The parameter key must not be null.");
}
bool contains = false;
try
{
contains = fileDb.GetRecordByKey(key, new string[0], false) != null;
}
catch
{
if (CheckReindex())
{
try
{
contains = fileDb.GetRecordByKey(key, new string[0], false) != null;
}
catch
{
CreateDatebase();
}
}
}
return contains;
}
public override object Get(string key, string regionName = null)
{
if (regionName != null)
{
throw new NotSupportedException("The parameter regionName must be null.");
}
if (key == null)
{
throw new ArgumentNullException("The parameter key must not be null.");
}
object value = null;
Record record = null;
try
{
record = fileDb.GetRecordByKey(key, new string[] { valueField }, false);
}
catch
{
if (CheckReindex())
{
try
{
record = fileDb.GetRecordByKey(key, new string[] { valueField }, false);
}
catch
{
CreateDatebase();
}
}
}
if (record != null)
{
try
{
using (MemoryStream stream = new MemoryStream((byte[])record[0]))
{
value = formatter.Deserialize(stream);
}
}
catch (Exception exc)
{
Trace.TraceWarning("FileDbCache.Get({0}): {1}", key, exc.Message);
try
{
fileDb.DeleteRecordByKey(key);
}
catch
{
}
}
}
return value;
}
public override CacheItem GetCacheItem(string key, string regionName = null)
{
var value = Get(key, regionName);
return value != null ? new CacheItem(key, value) : null;
}
public override IDictionary<string, object> GetValues(IEnumerable<string> keys, string regionName = null)
{
if (regionName != null)
{
throw new NotSupportedException("The parameter regionName must be null.");
}
var values = new Dictionary<string, object>();
foreach (string key in keys)
{
values[key] = Get(key);
}
return values;
}
public override void Set(string key, object value, CacheItemPolicy policy, string regionName = null)
{
if (regionName != null)
{
throw new NotSupportedException("The parameter regionName must be null.");
}
if (value == null)
{
throw new ArgumentNullException("The parameter value must not be null.");
}
if (key == null)
{
throw new ArgumentNullException("The parameter key must not be null.");
}
byte[] valueBuffer = null;
try
{
using (MemoryStream stream = new MemoryStream())
{
formatter.Serialize(stream, value);
valueBuffer = stream.ToArray();
}
}
catch (Exception exc)
{
Trace.TraceWarning("FileDbCache.Set({0}): {1}", key, exc.Message);
}
if (valueBuffer != null)
{
DateTime expires = DateTime.MaxValue;
if (policy.AbsoluteExpiration != InfiniteAbsoluteExpiration)
{
expires = policy.AbsoluteExpiration.DateTime;
}
else if (policy.SlidingExpiration != NoSlidingExpiration)
{
expires = DateTime.UtcNow + policy.SlidingExpiration;
}
try
{
AddOrUpdateRecord(key, valueBuffer, expires);
}
catch
{
if (CheckReindex())
{
try
{
AddOrUpdateRecord(key, valueBuffer, expires);
}
catch
{
CreateDatebase();
AddOrUpdateRecord(key, valueBuffer, expires);
}
}
}
}
}
public override void Set(CacheItem item, CacheItemPolicy policy)
{
Set(item.Key, item.Value, policy, item.RegionName);
}
public override void Set(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
{
Set(key, value, new CacheItemPolicy { AbsoluteExpiration = absoluteExpiration }, regionName);
}
public override object AddOrGetExisting(string key, object value, CacheItemPolicy policy, string regionName = null)
{
var oldValue = Get(key, regionName);
Set(key, value, policy);
return oldValue;
}
public override CacheItem AddOrGetExisting(CacheItem item, CacheItemPolicy policy)
{
var oldItem = GetCacheItem(item.Key, item.RegionName);
Set(item, policy);
return oldItem;
}
public override object AddOrGetExisting(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
{
return AddOrGetExisting(key, value, new CacheItemPolicy { AbsoluteExpiration = absoluteExpiration }, regionName);
}
public override object Remove(string key, string regionName = null)
{
var oldValue = Get(key, regionName);
if (oldValue != null)
{
try
{
fileDb.DeleteRecordByKey(key);
}
catch
{
}
}
return oldValue;
}
public void Flush()
{
try
{
fileDb.Flush();
}
catch
{
CheckReindex();
}
}
public void Clean()
{
try
{
fileDb.Clean();
}
catch
{
CheckReindex();
}
}
public void Dispose()
{
try
{
fileDb.DeleteRecords(new FilterExpression(expiresField, DateTime.UtcNow, EqualityEnum.LessThanOrEqual));
Trace.TraceInformation("FileDbCache has deleted {0} expired items", fileDb.NumDeleted);
fileDb.Clean();
fileDb.Close();
}
catch
{
if (CheckReindex())
{
fileDb.Close();
}
}
}
private bool CheckReindex()
{
if (fileDb.IsOpen)
{
Trace.TraceWarning("FileDbCache is reindexing database");
fileDb.Reindex();
return true;
}
return false;
}
private void CreateDatebase()
{
if (File.Exists(path))
{
File.Delete(path);
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
}
fileDb.Create(path, new Field[]
{
new Field(keyField, DataTypeEnum.String) { IsPrimaryKey = true },
new Field(valueField, DataTypeEnum.Byte) { IsArray = true },
new Field(expiresField, DataTypeEnum.DateTime)
});
}
private void AddOrUpdateRecord(string key, object value, DateTime expires)
{
var fieldValues = new FieldValues(3); // capacity
fieldValues.Add(valueField, value);
fieldValues.Add(expiresField, expires);
if (fileDb.GetRecordByKey(key, new string[0], false) == null)
{
fieldValues.Add(keyField, key);
fileDb.AddRecord(fieldValues);
}
else
{
fileDb.UpdateRecordByKey(key, fieldValues);
}
}
}
}

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{EF44F661-B98A-4676-927F-85D138F82300}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Caching</RootNamespace>
<AssemblyName>FileDbCache</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>none</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="FileDb, Version=3.10.0.0, Culture=neutral, PublicKeyToken=68cc942b9efb3282, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>FileDb\FileDb.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Caching" />
</ItemGroup>
<ItemGroup>
<Compile Include="FileDbCache.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="FileDb\FileDb.dll" />
<Content Include="FileDb\FileDb.txt" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- 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.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View file

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("FileDbCache")]
[assembly: AssemblyDescription("ObjectCache implementation based on EzTools FileDb")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("FileDbCache")]
[assembly: AssemblyCopyright("Copyright © 2012 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("c1fc4f52-e47c-4f62-9807-7e096db69851")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View file

@ -5,6 +5,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapControl", "MapControl\Ma
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication", "TestApplication\TestApplication.csproj", "{CCBCDAE5-E68F-43A8-930A-0749E476D29D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication", "TestApplication\TestApplication.csproj", "{CCBCDAE5-E68F-43A8-930A-0749E476D29D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileDbCache", "Caches\FileDbCache\FileDbCache.csproj", "{EF44F661-B98A-4676-927F-85D138F82300}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -35,6 +37,16 @@ Global
{CCBCDAE5-E68F-43A8-930A-0749E476D29D}.Release|Mixed Platforms.Build.0 = Release|x86 {CCBCDAE5-E68F-43A8-930A-0749E476D29D}.Release|Mixed Platforms.Build.0 = Release|x86
{CCBCDAE5-E68F-43A8-930A-0749E476D29D}.Release|x86.ActiveCfg = Release|x86 {CCBCDAE5-E68F-43A8-930A-0749E476D29D}.Release|x86.ActiveCfg = Release|x86
{CCBCDAE5-E68F-43A8-930A-0749E476D29D}.Release|x86.Build.0 = Release|x86 {CCBCDAE5-E68F-43A8-930A-0749E476D29D}.Release|x86.Build.0 = Release|x86
{EF44F661-B98A-4676-927F-85D138F82300}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF44F661-B98A-4676-927F-85D138F82300}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF44F661-B98A-4676-927F-85D138F82300}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{EF44F661-B98A-4676-927F-85D138F82300}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{EF44F661-B98A-4676-927F-85D138F82300}.Debug|x86.ActiveCfg = Debug|Any CPU
{EF44F661-B98A-4676-927F-85D138F82300}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF44F661-B98A-4676-927F-85D138F82300}.Release|Any CPU.Build.0 = Release|Any CPU
{EF44F661-B98A-4676-927F-85D138F82300}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{EF44F661-B98A-4676-927F-85D138F82300}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{EF44F661-B98A-4676-927F-85D138F82300}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -3,7 +3,6 @@
// Licensed under the Microsoft Public License (Ms-PL) // Licensed under the Microsoft Public License (Ms-PL)
using System; using System;
using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
@ -49,10 +48,5 @@ namespace MapControl
Brush.ImageSource = value; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Runtime.Caching; using System.Runtime.Caching;
using System.Threading; using System.Threading;
using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Threading; using System.Windows.Threading;
@ -16,28 +18,70 @@ using System.Windows.Threading;
namespace MapControl namespace MapControl
{ {
/// <summary> /// <summary>
/// Loads map tiles by their URIs and optionally caches their image files in a folder /// Loads map tile images by their URIs and optionally caches the images in an ObjectCache.
/// defined by the static TileCacheFolder property.
/// </summary> /// </summary>
public class TileImageLoader : DispatcherObject public class TileImageLoader : DispatcherObject
{ {
public static string TileCacheFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl Cache"); [Serializable]
public static TimeSpan TileCacheExpiryAge = TimeSpan.FromDays(1d); 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 TileLayer tileLayer;
private readonly Queue<Tile> pendingTiles = new Queue<Tile>(); private readonly Queue<Tile> pendingTiles = new Queue<Tile>();
private readonly HashSet<HttpWebRequest> currentRequests = new HashSet<HttpWebRequest>();
private int numDownloads; 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) public TileImageLoader(TileLayer tileLayer)
{ {
this.tileLayer = tileLayer; this.tileLayer = tileLayer;
} }
private bool IsCached
{
get { return tileLayer.IsCached && !string.IsNullOrEmpty(TileCacheFolder); }
}
internal void StartDownloadTiles(ICollection<Tile> tiles) internal void StartDownloadTiles(ICollection<Tile> tiles)
{ {
ThreadPool.QueueUserWorkItem(StartDownloadTilesAsync, new List<Tile>(tiles.Where(t => t.Image == null && t.Uri == null))); ThreadPool.QueueUserWorkItem(StartDownloadTilesAsync, new List<Tile>(tiles.Where(t => t.Image == null && t.Uri == null)));
@ -49,46 +93,54 @@ namespace MapControl
{ {
pendingTiles.Clear(); pendingTiles.Clear();
} }
lock (currentRequests)
{
foreach (HttpWebRequest request in currentRequests)
{
request.Abort();
}
}
} }
private void StartDownloadTilesAsync(object newTilesList) private void StartDownloadTilesAsync(object newTilesList)
{ {
List<Tile> newTiles = (List<Tile>)newTilesList; List<Tile> newTiles = (List<Tile>)newTilesList;
List<Tile> expiredTiles = new List<Tile>(newTiles.Count);
lock (pendingTiles) 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; string key = CacheKey(tile);
image = GetFileCachedImage(tile, out fileCacheExpired); CachedImage cachedImage = Cache.Get(key) as CachedImage;
if (image != null) if (cachedImage == null)
{ {
SetMemoryCachedImage(tile, image); pendingTiles.Enqueue(tile);
if (fileCacheExpired)
{
expiredTiles.Add(tile); // enqueue later
}
} }
} 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) outdatedTiles.ForEach(tile => pendingTiles.Enqueue(tile));
{ }
Dispatcher.BeginInvoke((Action)(() => tile.Image = image));
}
else
{
pendingTiles.Enqueue(tile);
}
});
expiredTiles.ForEach(tile => pendingTiles.Enqueue(tile));
DownloadNextTiles(null); DownloadNextTiles(null);
} }
@ -109,13 +161,13 @@ namespace MapControl
private void DownloadTileAsync(object t) private void DownloadTileAsync(object t)
{ {
Tile tile = (Tile)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); Cache.Set(CacheKey(tile), new CachedImage(imageBuffer), new CacheItemPolicy { SlidingExpiration = CacheExpiration });
Dispatcher.BeginInvoke((Action)(() => tile.Image = image));
} }
lock (pendingTiles) 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}", HttpWebRequest request = null;
Path.Combine(TileCacheFolder, tileLayer.Name, tile.ZoomLevel.ToString(), tile.XIndex.ToString(), tile.Y.ToString()), byte[] buffer = null;
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;
try try
{ {
TraceInformation("{0} - Requesting", tile.Uri); TraceInformation("{0} - Requesting", tile.Uri);
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(tile.Uri); request = (HttpWebRequest)WebRequest.Create(tile.Uri);
webRequest.UserAgent = typeof(TileImageLoader).ToString(); request.UserAgent = typeof(TileImageLoader).ToString();
webRequest.KeepAlive = true; 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 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); 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) catch (WebException exc)
{ {
buffer = null;
if (exc.Status == WebExceptionStatus.ProtocolError) if (exc.Status == WebExceptionStatus.ProtocolError)
{ {
TraceInformation("{0} - {1}", tile.Uri, ((HttpWebResponse)exc.Response).StatusCode); TraceInformation("{0} - {1}", tile.Uri, ((HttpWebResponse)exc.Response).StatusCode);
} }
else if (exc.Status == WebExceptionStatus.RequestCanceled)
{
TraceInformation("{0} - {1}", tile.Uri, exc.Status);
}
else else
{ {
TraceWarning("{0} - {1}", tile.Uri, exc.Status); TraceWarning("{0} - {1}", tile.Uri, exc.Status);
@ -236,20 +234,56 @@ namespace MapControl
} }
catch (Exception exc) catch (Exception exc)
{ {
buffer = null;
TraceWarning("{0} - {1}", tile.Uri, exc.Message); 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) 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) 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 namespace MapControl
{ {
/// <summary> /// <summary>
/// Fills a rectangular area with map tiles from a TileSource. If the IsCached property is true, /// Fills a rectangular area with map tiles from a TileSource.
/// map tiles are cached in a folder defined by the TileImageLoader.TileCacheFolder property.
/// </summary> /// </summary>
[ContentProperty("TileSource")] [ContentProperty("TileSource")]
public class TileLayer : DrawingVisual public class TileLayer : DrawingVisual
@ -30,19 +29,16 @@ namespace MapControl
VisualEdgeMode = EdgeMode.Aliased; VisualEdgeMode = EdgeMode.Aliased;
VisualTransform = new MatrixTransform(); VisualTransform = new MatrixTransform();
Name = string.Empty; Name = string.Empty;
ImageType = "png";
MinZoomLevel = 1; MinZoomLevel = 1;
MaxZoomLevel = 18; MaxZoomLevel = 18;
MaxDownloads = 8; MaxDownloads = 8;
} }
public string Name { get; set; } public string Name { get; set; }
public string ImageType { get; set; }
public TileSource TileSource { get; set; } public TileSource TileSource { get; set; }
public int MinZoomLevel { get; set; } public int MinZoomLevel { get; set; }
public int MaxZoomLevel { get; set; } public int MaxZoomLevel { get; set; }
public int MaxDownloads { get; set; } public int MaxDownloads { get; set; }
public bool IsCached { get; set; }
public bool HasDarkBackground { get; set; } public bool HasDarkBackground { get; set; }
public string Description public string Description
@ -139,7 +135,7 @@ namespace MapControl
drawingContext.DrawRectangle(tile.Brush, null, tileRect); drawingContext.DrawRectangle(tile.Brush, null, tileRect);
//if (tile.ZoomLevel == zoomLevel) //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);
}); });
} }
} }

View file

@ -7,8 +7,7 @@
<Window.Resources> <Window.Resources>
<map:TileLayer x:Key="SeamarksTileLayer" <map:TileLayer x:Key="SeamarksTileLayer"
Name="Seamarks" Description="© {y} OpenSeaMap Contributors, CC-BY-SA" Name="Seamarks" Description="© {y} OpenSeaMap Contributors, CC-BY-SA"
TileSource="http://tiles.openseamap.org/seamark/{z}/{x}/{y}.png" TileSource="http://tiles.openseamap.org/seamark/{z}/{x}/{y}.png" MinZoomLevel="10" MaxZoomLevel="18"/>
IsCached="False" MinZoomLevel="10" MaxZoomLevel="18"/>
<local:SampleItemCollection x:Key="Polylines"/> <local:SampleItemCollection x:Key="Polylines"/>
<local:SampleItemCollection x:Key="Points"/> <local:SampleItemCollection x:Key="Points"/>
<local:SampleItemCollection x:Key="Pushpins"/> <local:SampleItemCollection x:Key="Pushpins"/>
@ -120,30 +119,25 @@
<ComboBox Name="tileLayerComboBox" ToolTip="Main Tile Layer" Margin="4,0,4,0" DisplayMemberPath="Name" SelectedIndex="0"> <ComboBox Name="tileLayerComboBox" ToolTip="Main Tile Layer" Margin="4,0,4,0" DisplayMemberPath="Name" SelectedIndex="0">
<ComboBox.Items> <ComboBox.Items>
<map:TileLayer Name="OpenStreetMap" Description="© {y} OpenStreetMap Contributors, CC-BY-SA" <map:TileLayer Name="OpenStreetMap" Description="© {y} OpenStreetMap Contributors, CC-BY-SA"
TileSource="http://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png" IsCached="False"/> TileSource="http://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png"/>
<map:TileLayer Name="OpenCycleMap" Description="OpenCycleMap - © {y} Andy Allen &amp; OpenStreetMap Contributors, CC-BY-SA" <map:TileLayer Name="OpenCycleMap" Description="OpenCycleMap - © {y} Andy Allen &amp; OpenStreetMap Contributors, CC-BY-SA"
TileSource="http://{c}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png" IsCached="False"/> TileSource="http://{c}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png"/>
<map:TileLayer Name="OCM Transport" Description="OpenCycleMap Transport - © {y} Andy Allen &amp; OpenStreetMap Contributors, CC-BY-SA" <map:TileLayer Name="OCM Transport" Description="OpenCycleMap Transport - © {y} Andy Allen &amp; OpenStreetMap Contributors, CC-BY-SA"
TileSource="http://{c}.tile2.opencyclemap.org/transport/{z}/{x}/{y}.png" IsCached="False"/> TileSource="http://{c}.tile2.opencyclemap.org/transport/{z}/{x}/{y}.png"/>
<map:TileLayer Name="OCM Landscape" Description="OpenCycleMap Landscape - © {y} Andy Allen &amp; OpenStreetMap Contributors, CC-BY-SA" <map:TileLayer Name="OCM Landscape" Description="OpenCycleMap Landscape - © {y} Andy Allen &amp; OpenStreetMap Contributors, CC-BY-SA"
TileSource="http://{c}.tile3.opencyclemap.org/landscape/{z}/{x}/{y}.png" IsCached="False"/> TileSource="http://{c}.tile3.opencyclemap.org/landscape/{z}/{x}/{y}.png"/>
<map:TileLayer Name="MapQuest OSM" Description="MapQuest OSM - © {y} MapQuest &amp; OpenStreetMap Contributors" <map:TileLayer Name="MapQuest OSM" Description="MapQuest OSM - © {y} MapQuest &amp; OpenStreetMap Contributors"
TileSource="http://otile{n}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png" IsCached="False"/> TileSource="http://otile{n}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png"/>
<!--<map:TileLayer Name="Google Maps" Description="Google Maps - © {y} Google" <!--<map:TileLayer Name="Google Maps" Description="Google Maps - © {y} Google"
TileSource="http://mt{i}.google.com/vt/x={x}&amp;y={y}&amp;z={z}" TileSource="http://mt{i}.google.com/vt/x={x}&amp;y={y}&amp;z={z}" MaxZoomLevel="20"/>
IsCached="False" MaxZoomLevel="20"/>
<map:TileLayer Name="Google Images" Description="Google Maps - © {y} Google" <map:TileLayer Name="Google Images" Description="Google Maps - © {y} Google"
TileSource="http://khm{i}.google.com/kh/v=113&amp;x={x}&amp;y={y}&amp;z={z}" TileSource="http://khm{i}.google.com/kh/v=113&amp;x={x}&amp;y={y}&amp;z={z}" MaxZoomLevel="20" HasDarkBackground="True"/>
ImageType="jpg" IsCached="False" MaxZoomLevel="20" HasDarkBackground="True"/>
<map:TileLayer Name="Bing Maps" Description="Bing Maps - © {y} Microsoft Corporation" <map:TileLayer Name="Bing Maps" Description="Bing Maps - © {y} Microsoft Corporation"
TileSource="http://ecn.t{i}.tiles.virtualearth.net/tiles/r{q}.png?g=0&amp;stl=h" TileSource="http://ecn.t{i}.tiles.virtualearth.net/tiles/r{q}.png?g=0&amp;stl=h" MaxZoomLevel="20"/>
IsCached="False" MaxZoomLevel="20"/>
<map:TileLayer Name="Bing Images" Description="Bing Maps - © {y} Microsoft Corporation" <map:TileLayer Name="Bing Images" Description="Bing Maps - © {y} Microsoft Corporation"
TileSource="http://ecn.t{i}.tiles.virtualearth.net/tiles/a{q}.jpeg?g=0" TileSource="http://ecn.t{i}.tiles.virtualearth.net/tiles/a{q}.jpeg?g=0" MaxZoomLevel="20" HasDarkBackground="True"/>
ImageType="jpg" IsCached="False" MaxZoomLevel="20" HasDarkBackground="True"/>
<map:TileLayer Name="Bing Hybrid" Description="Bing Maps - © {y} Microsoft Corporation" <map:TileLayer Name="Bing Hybrid" Description="Bing Maps - © {y} Microsoft Corporation"
TileSource="http://ecn.t{i}.tiles.virtualearth.net/tiles/h{q}.jpeg?g=0&amp;stl=h" TileSource="http://ecn.t{i}.tiles.virtualearth.net/tiles/h{q}.jpeg?g=0&amp;stl=h" MaxZoomLevel="20" HasDarkBackground="True"/>-->
ImageType="jpg" IsCached="False" MaxZoomLevel="20" HasDarkBackground="True"/>-->
</ComboBox.Items> </ComboBox.Items>
</ComboBox> </ComboBox>
</StackPanel> </StackPanel>

View file

@ -1,11 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using MapControl; using MapControl;
using Caching;
namespace MapControlTestApp namespace MapControlTestApp
{ {
@ -13,6 +15,15 @@ namespace MapControlTestApp
{ {
public MainWindow() public MainWindow()
{ {
bool usePersistentCache = false;
if (usePersistentCache)
{
string appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
string cachePath = Path.Combine(appDataFolder, "MapControl", "Cache.fdb");
TileImageLoader.Cache = new FileDbCache("MapControlCache", cachePath) { AutoFlush = false };
}
InitializeComponent(); InitializeComponent();
ICollection<object> polylines = (ICollection<object>)Resources["Polylines"]; ICollection<object> polylines = (ICollection<object>)Resources["Polylines"];

View file

@ -41,6 +41,7 @@
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Runtime.Caching" />
<Reference Include="System.Xaml"> <Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework> <RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference> </Reference>
@ -74,6 +75,10 @@
<AppDesigner Include="Properties\" /> <AppDesigner Include="Properties\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Caches\FileDbCache\FileDbCache.csproj">
<Project>{EF44F661-B98A-4676-927F-85D138F82300}</Project>
<Name>FileDbCache</Name>
</ProjectReference>
<ProjectReference Include="..\MapControl\MapControl.csproj"> <ProjectReference Include="..\MapControl\MapControl.csproj">
<Project>{06481252-2310-414A-B9FC-D5739FDF6BD3}</Project> <Project>{06481252-2310-414A-B9FC-D5739FDF6BD3}</Project>
<Name>MapControl</Name> <Name>MapControl</Name>