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-08-06 17:41:39 +02:00
|
|
|
|
using System.Collections.Concurrent;
|
2012-04-25 22:02:53 +02:00
|
|
|
|
using System.Collections.Generic;
|
2012-07-03 18:03:56 +02:00
|
|
|
|
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.Media.Imaging;
|
|
|
|
|
|
using System.Windows.Threading;
|
|
|
|
|
|
|
|
|
|
|
|
namespace MapControl
|
|
|
|
|
|
{
|
2012-05-04 12:52:20 +02:00
|
|
|
|
/// <summary>
|
2012-07-03 18:03:56 +02:00
|
|
|
|
/// 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-08-24 18:10:12 +02:00
|
|
|
|
public class TileImageLoader
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-06-24 23:42:11 +02:00
|
|
|
|
private readonly TileLayer tileLayer;
|
2012-08-06 17:41:39 +02:00
|
|
|
|
private readonly ConcurrentQueue<Tile> pendingTiles = new ConcurrentQueue<Tile>();
|
2012-08-24 18:10:12 +02:00
|
|
|
|
private int downloadThreadCount;
|
2012-04-25 22:02:53 +02:00
|
|
|
|
|
2012-08-15 21:31:10 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Default Name of an ObjectCache instance that is assigned to the Cache property.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static readonly string DefaultCacheName = "TileCache";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Default value for the directory where an ObjectCache instance may save cached data.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static readonly string DefaultCacheDirectory =
|
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl");
|
|
|
|
|
|
|
2012-07-03 18:03:56 +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.
|
2012-07-03 18:03:56 +02:00
|
|
|
|
/// </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.
|
2012-07-03 18:03:56 +02:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static TimeSpan CacheUpdateAge { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
static TileImageLoader()
|
2012-06-24 23:42:11 +02:00
|
|
|
|
{
|
2012-07-03 18:03:56 +02:00
|
|
|
|
Cache = MemoryCache.Default;
|
|
|
|
|
|
CacheExpiration = TimeSpan.FromDays(30d);
|
|
|
|
|
|
CacheUpdateAge = TimeSpan.FromDays(1d);
|
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
|
|
|
|
{
|
2012-07-03 18:03:56 +02:00
|
|
|
|
this.tileLayer = tileLayer;
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2012-07-20 21:57:29 +02:00
|
|
|
|
internal void BeginGetTiles(ICollection<Tile> tiles)
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-07-20 21:57:29 +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
|
|
|
|
}
|
|
|
|
|
|
|
2012-07-20 21:57:29 +02:00
|
|
|
|
internal void CancelGetTiles()
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-08-06 17:41:39 +02:00
|
|
|
|
Tile tile;
|
|
|
|
|
|
while (pendingTiles.TryDequeue(out tile)) ; // no Clear method
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2012-07-20 21:57:29 +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;
|
|
|
|
|
|
|
2012-07-20 21:57:29 +02:00
|
|
|
|
newTiles.ForEach(tile =>
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-11-04 10:16:39 +01:00
|
|
|
|
tileLayer.Dispatcher.BeginInvoke(DispatcherPriority.Background,
|
|
|
|
|
|
(Action)(() => tile.Image = imageTileSource.GetImage(tile.XIndex, tile.Y, tile.ZoomLevel)));
|
2012-07-20 21:57:29 +02:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2012-08-06 17:41:39 +02:00
|
|
|
|
if (Cache == null)
|
2012-07-03 18:03:56 +02:00
|
|
|
|
{
|
2012-08-06 17:41:39 +02:00
|
|
|
|
newTiles.ForEach(tile => pendingTiles.Enqueue(tile));
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
List<Tile> outdatedTiles = new List<Tile>(newTiles.Count);
|
|
|
|
|
|
|
|
|
|
|
|
newTiles.ForEach(tile =>
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-08-06 17:41:39 +02:00
|
|
|
|
string key = CacheKey(tile);
|
2012-08-24 18:10:12 +02:00
|
|
|
|
byte[] buffer = Cache.Get(key) as byte[];
|
2012-04-25 22:02:53 +02:00
|
|
|
|
|
2012-08-24 18:10:12 +02:00
|
|
|
|
if (buffer == null)
|
2012-07-03 18:03:56 +02:00
|
|
|
|
{
|
2012-08-06 17:41:39 +02:00
|
|
|
|
pendingTiles.Enqueue(tile);
|
|
|
|
|
|
}
|
2012-08-24 18:10:12 +02:00
|
|
|
|
else if (!CreateTileImage(tile, buffer))
|
2012-08-06 17:41:39 +02:00
|
|
|
|
{
|
|
|
|
|
|
// got corrupted buffer from cache
|
|
|
|
|
|
Cache.Remove(key);
|
|
|
|
|
|
pendingTiles.Enqueue(tile);
|
|
|
|
|
|
}
|
2012-08-24 18:10:12 +02:00
|
|
|
|
else if (IsCacheOutdated(buffer))
|
2012-08-06 17:41:39 +02:00
|
|
|
|
{
|
|
|
|
|
|
// update cached image
|
|
|
|
|
|
outdatedTiles.Add(tile);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
outdatedTiles.ForEach(tile => pendingTiles.Enqueue(tile));
|
|
|
|
|
|
}
|
2012-06-24 23:42:11 +02:00
|
|
|
|
|
2012-08-24 18:10:12 +02:00
|
|
|
|
while (downloadThreadCount < Math.Min(pendingTiles.Count, tileLayer.MaxParallelDownloads))
|
2012-08-06 17:41:39 +02:00
|
|
|
|
{
|
2012-08-24 18:10:12 +02:00
|
|
|
|
Interlocked.Increment(ref downloadThreadCount);
|
|
|
|
|
|
|
2012-08-06 17:41:39 +02:00
|
|
|
|
ThreadPool.QueueUserWorkItem(DownloadTiles);
|
2012-07-20 21:57:29 +02:00
|
|
|
|
}
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2012-07-20 21:57:29 +02:00
|
|
|
|
private void DownloadTiles(object o)
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-08-06 17:41:39 +02:00
|
|
|
|
Tile tile;
|
|
|
|
|
|
while (pendingTiles.TryDequeue(out tile))
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-07-20 21:57:29 +02:00
|
|
|
|
tile.Uri = tileLayer.TileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
|
2012-08-24 18:10:12 +02:00
|
|
|
|
byte[] buffer = DownloadImage(tile.Uri);
|
2012-04-25 22:02:53 +02:00
|
|
|
|
|
2012-08-24 18:10:12 +02:00
|
|
|
|
if (buffer != null && CreateTileImage(tile, buffer) && Cache != null)
|
2012-07-20 21:57:29 +02:00
|
|
|
|
{
|
2012-08-24 18:10:12 +02:00
|
|
|
|
Cache.Set(CacheKey(tile), buffer, new CacheItemPolicy { SlidingExpiration = CacheExpiration });
|
2012-07-20 21:57:29 +02:00
|
|
|
|
}
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
2012-08-24 18:10:12 +02:00
|
|
|
|
|
|
|
|
|
|
Interlocked.Decrement(ref downloadThreadCount);
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2012-07-03 18:03:56 +02:00
|
|
|
|
private string CacheKey(Tile tile)
|
2012-06-24 23:42:11 +02:00
|
|
|
|
{
|
2012-11-04 10:16:39 +01:00
|
|
|
|
return string.Format("{0}/{1}/{2}/{3}", tileLayer.SourceName, tile.ZoomLevel, tile.XIndex, tile.Y);
|
2012-06-24 23:42:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2012-08-24 18:10:12 +02:00
|
|
|
|
private bool CreateTileImage(Tile tile, byte[] buffer)
|
2012-06-24 23:42:11 +02:00
|
|
|
|
{
|
2012-08-24 18:10:12 +02:00
|
|
|
|
BitmapImage bitmap = new BitmapImage();
|
2012-06-24 23:42:11 +02:00
|
|
|
|
|
2012-07-03 18:03:56 +02:00
|
|
|
|
try
|
2012-06-24 23:42:11 +02:00
|
|
|
|
{
|
2012-08-24 18:10:12 +02:00
|
|
|
|
using (Stream stream = new MemoryStream(buffer, 8, buffer.Length - 8, false))
|
|
|
|
|
|
{
|
|
|
|
|
|
bitmap.BeginInit();
|
|
|
|
|
|
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
|
|
|
|
|
bitmap.StreamSource = stream;
|
|
|
|
|
|
bitmap.EndInit();
|
|
|
|
|
|
bitmap.Freeze();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Trace.TraceWarning("Creating tile image failed: {0}", ex.Message);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tileLayer.Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)(() => tile.Image = bitmap));
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2012-06-24 23:42:11 +02:00
|
|
|
|
|
2012-08-24 18:10:12 +02:00
|
|
|
|
private static byte[] DownloadImage(Uri uri)
|
|
|
|
|
|
{
|
|
|
|
|
|
byte[] buffer = null;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
|
2012-07-03 18:03:56 +02:00
|
|
|
|
request.UserAgent = typeof(TileImageLoader).ToString();
|
2012-04-25 22:02:53 +02:00
|
|
|
|
|
2012-07-03 18:03:56 +02:00
|
|
|
|
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
|
2012-07-27 18:47:57 +02:00
|
|
|
|
using (Stream responseStream = response.GetResponseStream())
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-08-15 21:31:10 +02:00
|
|
|
|
long length = response.ContentLength;
|
|
|
|
|
|
long creationTime = DateTime.UtcNow.ToBinary();
|
|
|
|
|
|
|
|
|
|
|
|
using (MemoryStream memoryStream = length > 0 ? new MemoryStream((int)length + 8) : new MemoryStream())
|
2012-07-27 18:47:57 +02:00
|
|
|
|
{
|
2012-08-15 21:31:10 +02:00
|
|
|
|
memoryStream.Write(BitConverter.GetBytes(creationTime), 0, 8);
|
|
|
|
|
|
responseStream.CopyTo(memoryStream);
|
|
|
|
|
|
|
|
|
|
|
|
buffer = length > 0 ? memoryStream.GetBuffer() : memoryStream.ToArray();
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2012-08-24 18:10:12 +02:00
|
|
|
|
Trace.TraceInformation("Downloaded {0}", uri);
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
2012-07-07 17:19:10 +02:00
|
|
|
|
catch (WebException ex)
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-07-07 17:19:10 +02:00
|
|
|
|
if (ex.Status == WebExceptionStatus.ProtocolError)
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-08-24 18:10:12 +02:00
|
|
|
|
HttpStatusCode statusCode = ((HttpWebResponse)ex.Response).StatusCode;
|
|
|
|
|
|
if (statusCode != HttpStatusCode.NotFound)
|
|
|
|
|
|
{
|
|
|
|
|
|
Trace.TraceInformation("Downloading {0} failed: {1}", uri, ex.Message);
|
|
|
|
|
|
}
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2012-08-24 18:10:12 +02:00
|
|
|
|
Trace.TraceWarning("Downloading {0} failed with {1}: {2}", uri, ex.Status, ex.Message);
|
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
|
|
|
|
{
|
2012-08-24 18:10:12 +02:00
|
|
|
|
Trace.TraceWarning("Downloading {0} failed: {1}", uri, ex.Message);
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2012-07-03 18:03:56 +02:00
|
|
|
|
return buffer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2012-08-24 18:10:12 +02:00
|
|
|
|
private static bool IsCacheOutdated(byte[] buffer)
|
2012-08-15 21:31:10 +02:00
|
|
|
|
{
|
2012-08-24 18:10:12 +02:00
|
|
|
|
long creationTime = BitConverter.ToInt64(buffer, 0);
|
2012-08-15 21:31:10 +02:00
|
|
|
|
|
|
|
|
|
|
return DateTime.FromBinary(creationTime) + CacheUpdateAge < DateTime.UtcNow;
|
|
|
|
|
|
}
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|