XAML-Map-Control/MapControl/TileImageLoader.cs

335 lines
12 KiB
C#
Raw Normal View History

2012-05-04 12:52:20 +02:00
// WPF MapControl - http://wpfmapcontrol.codeplex.com/
// Copyright © 2012 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
2012-04-25 22:02:53 +02:00
using System.Collections.Generic;
2012-07-07 17:19:10 +02:00
using System.Collections.Specialized;
using System.Diagnostics;
2012-04-25 22:02:53 +02:00
using System.IO;
using System.Linq;
using System.Net;
2012-06-24 23:42:11 +02:00
using System.Runtime.Caching;
2012-04-25 22:02:53 +02:00
using System.Threading;
using System.Windows;
2012-04-25 22:02:53 +02:00
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace MapControl
{
2012-05-04 12:52:20 +02:00
/// <summary>
/// Loads map tile images by their URIs and optionally caches the images in an ObjectCache.
2012-05-04 12:52:20 +02:00
/// </summary>
2012-04-25 22:02:53 +02:00
public class TileImageLoader : DispatcherObject
{
[Serializable]
private class CachedImage
{
public readonly DateTime CreationTime = DateTime.UtcNow;
public readonly byte[] ImageBuffer;
public CachedImage(byte[] imageBuffer)
{
ImageBuffer = imageBuffer;
}
}
2012-04-25 22:02:53 +02:00
2012-06-24 23:42:11 +02:00
private readonly TileLayer tileLayer;
2012-04-25 22:02:53 +02:00
private readonly Queue<Tile> pendingTiles = new Queue<Tile>();
private readonly HashSet<HttpWebRequest> currentRequests = new HashSet<HttpWebRequest>();
2012-04-25 22:02:53 +02:00
/// <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
2012-07-07 17:19:10 +02:00
/// 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 a 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.
2012-07-07 17:19:10 +02:00
/// 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; }
2012-07-07 17:19:10 +02:00
/// <summary>
/// Creates an instance of the ObjectCache-derived type T and sets the static Cache
/// property to this instance. Class T must (like System.Runtime.Caching.MemoryCache)
/// provide a constructor with two parameters, first a string that gets the name of
/// the cache instance, second a NameValueCollection that gets the config parameter.
/// If config is null, a new NameValueCollection is created. If config does not already
/// contain an entry with key "directory", a new entry is added with this key and a
/// value that specifies the path to an application data directory where the cache
/// implementation may store persistent cache data files.
/// </summary>
public static void CreateCache<T>(NameValueCollection config = null) where T : ObjectCache
{
if (config == null)
{
config = new NameValueCollection(1);
}
if (config["directory"] == null)
{
config["directory"] = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl");
}
try
{
Cache = (ObjectCache)Activator.CreateInstance(typeof(T), "TileCache", config);
}
catch (Exception ex)
{
Trace.TraceWarning("Could not create instance of type {0} with String and NameValueCollection constructor parameters: {1}", typeof(T), ex.Message);
throw;
}
}
static TileImageLoader()
2012-06-24 23:42:11 +02:00
{
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();
}
};
2012-06-24 23:42:11 +02:00
}
2012-04-25 22:02:53 +02:00
2012-07-07 17:19:10 +02:00
internal TileImageLoader(TileLayer tileLayer)
2012-04-25 22:02:53 +02:00
{
this.tileLayer = tileLayer;
2012-04-25 22:02:53 +02:00
}
internal void BeginGetTiles(ICollection<Tile> tiles)
2012-04-25 22:02:53 +02:00
{
ThreadPool.QueueUserWorkItem(BeginGetTilesAsync, new List<Tile>(tiles.Where(t => t.Image == null && t.Uri == null)));
2012-04-25 22:02:53 +02:00
}
internal void CancelGetTiles()
2012-04-25 22:02:53 +02:00
{
lock (pendingTiles)
{
pendingTiles.Clear();
}
lock (currentRequests)
{
foreach (HttpWebRequest request in currentRequests)
{
request.Abort();
}
}
2012-04-25 22:02:53 +02:00
}
private void BeginGetTilesAsync(object newTilesList)
2012-04-25 22:02:53 +02:00
{
List<Tile> newTiles = (List<Tile>)newTilesList;
2012-07-21 09:09:58 +02:00
if (tileLayer.TileSource is ImageTileSource)
2012-04-25 22:02:53 +02:00
{
2012-07-21 09:09:58 +02:00
ImageTileSource imageTileSource = (ImageTileSource)tileLayer.TileSource;
newTiles.ForEach(tile =>
2012-04-25 22:02:53 +02:00
{
Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)(() => tile.Image = imageTileSource.GetImage(tile.XIndex, tile.Y, tile.ZoomLevel)));
});
}
else
{
lock (pendingTiles)
{
if (Cache == null)
{
newTiles.ForEach(tile => pendingTiles.Enqueue(tile));
}
else
2012-04-25 22:02:53 +02:00
{
List<Tile> outdatedTiles = new List<Tile>(newTiles.Count);
2012-04-25 22:02:53 +02:00
newTiles.ForEach(tile =>
{
string key = CacheKey(tile);
CachedImage cachedImage = Cache.Get(key) as CachedImage;
if (cachedImage == null)
{
pendingTiles.Enqueue(tile);
}
else if (!CreateTileImage(tile, cachedImage.ImageBuffer))
{
// got corrupted buffer from cache
Cache.Remove(key);
pendingTiles.Enqueue(tile);
}
else if (cachedImage.CreationTime + CacheUpdateAge < DateTime.UtcNow)
{
// update cached image
outdatedTiles.Add(tile);
}
});
outdatedTiles.ForEach(tile => pendingTiles.Enqueue(tile));
}
2012-06-24 23:42:11 +02:00
int numDownloads = Math.Min(pendingTiles.Count, tileLayer.MaxParallelDownloads);
2012-04-25 22:02:53 +02:00
while (--numDownloads >= 0)
{
ThreadPool.QueueUserWorkItem(DownloadTiles);
}
}
2012-04-25 22:02:53 +02:00
}
}
private void DownloadTiles(object o)
2012-04-25 22:02:53 +02:00
{
while (pendingTiles.Count > 0)
2012-04-25 22:02:53 +02:00
{
Tile tile;
2012-04-25 22:02:53 +02:00
lock (pendingTiles)
{
if (pendingTiles.Count == 0)
{
break;
}
2012-04-25 22:02:53 +02:00
tile = pendingTiles.Dequeue();
}
2012-04-25 22:02:53 +02:00
tile.Uri = tileLayer.TileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
byte[] imageBuffer = DownloadImage(tile);
2012-04-25 22:02:53 +02:00
if (imageBuffer != null &&
CreateTileImage(tile, imageBuffer) &&
Cache != null)
{
Cache.Set(CacheKey(tile), new CachedImage(imageBuffer), new CacheItemPolicy { SlidingExpiration = CacheExpiration });
}
2012-04-25 22:02:53 +02:00
}
}
private string CacheKey(Tile tile)
2012-06-24 23:42:11 +02:00
{
return string.Format("{0}-{1}-{2}-{3}", tileLayer.Name, tile.ZoomLevel, tile.XIndex, tile.Y);
2012-06-24 23:42:11 +02:00
}
private byte[] DownloadImage(Tile tile)
2012-06-24 23:42:11 +02:00
{
HttpWebRequest request = null;
byte[] buffer = null;
2012-06-24 23:42:11 +02:00
try
2012-06-24 23:42:11 +02:00
{
TraceInformation("{0} - Requesting", tile.Uri);
2012-06-24 23:42:11 +02:00
request = (HttpWebRequest)WebRequest.Create(tile.Uri);
request.UserAgent = typeof(TileImageLoader).ToString();
request.KeepAlive = true;
2012-04-25 22:02:53 +02:00
lock (currentRequests)
2012-04-25 22:02:53 +02:00
{
currentRequests.Add(request);
2012-04-25 22:02:53 +02:00
}
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
2012-04-25 22:02:53 +02:00
{
using (Stream responseStream = response.GetResponseStream())
{
buffer = new byte[(int)response.ContentLength];
using (MemoryStream memoryStream = new MemoryStream(buffer))
2012-04-25 22:02:53 +02:00
{
responseStream.CopyTo(memoryStream);
}
}
}
TraceInformation("{0} - Completed", tile.Uri);
}
2012-07-07 17:19:10 +02:00
catch (WebException ex)
2012-04-25 22:02:53 +02:00
{
buffer = null;
2012-07-07 17:19:10 +02:00
if (ex.Status == WebExceptionStatus.ProtocolError)
2012-04-25 22:02:53 +02:00
{
2012-07-07 17:19:10 +02:00
TraceInformation("{0} - {1}", tile.Uri, ((HttpWebResponse)ex.Response).StatusCode);
2012-04-25 22:02:53 +02:00
}
else if (ex.Status == WebExceptionStatus.RequestCanceled) // by HttpWebRequest.Abort in CancelGetTiles
{
2012-07-07 17:19:10 +02:00
TraceInformation("{0} - {1}", tile.Uri, ex.Status);
}
2012-04-25 22:02:53 +02:00
else
{
2012-07-07 17:19:10 +02:00
TraceWarning("{0} - {1}", tile.Uri, ex.Status);
2012-04-25 22:02:53 +02:00
}
}
2012-07-07 17:19:10 +02:00
catch (Exception ex)
2012-04-25 22:02:53 +02:00
{
buffer = null;
2012-07-07 17:19:10 +02:00
TraceWarning("{0} - {1}", tile.Uri, ex.Message);
2012-04-25 22:02:53 +02:00
}
if (request != null)
{
lock (currentRequests)
{
currentRequests.Remove(request);
}
}
return buffer;
}
private bool CreateTileImage(Tile tile, byte[] buffer)
{
BitmapImage bitmap = new BitmapImage();
try
{
using (Stream stream = new MemoryStream(buffer))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}
}
2012-07-07 17:19:10 +02:00
catch (Exception ex)
{
2012-07-07 17:19:10 +02:00
TraceWarning("Creating tile image failed: {0}", ex.Message);
return false;
}
Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)(() => tile.Image = bitmap));
return true;
2012-04-25 22:02:53 +02:00
}
private static void TraceWarning(string format, params object[] args)
{
Trace.TraceWarning("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args));
2012-04-25 22:02:53 +02:00
}
private static void TraceInformation(string format, params object[] args)
{
2012-07-04 17:19:48 +02:00
//Trace.TraceInformation("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args));
2012-04-25 22:02:53 +02:00
}
}
}