// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Caching;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace MapControl
{
///
/// Loads map tile images and optionally caches them in a System.Runtime.Caching.ObjectCache.
///
public class TileImageLoader : ITileImageLoader
{
///
/// Default name of an ObjectCache instance that is assigned to the Cache property.
///
public const string DefaultCacheName = "TileCache";
///
/// Default folder path where an ObjectCache instance may save cached data.
///
public static readonly string DefaultCacheFolder =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl");
///
/// Default expiration time for cached tile images. Used when no expiration time
/// was transmitted on download. The default and recommended minimum value is seven days.
/// See OpenStreetMap tile usage policy: http://wiki.openstreetmap.org/wiki/Tile_usage_policy
///
public static TimeSpan DefaultCacheExpiration = TimeSpan.FromDays(7);
///
/// The ObjectCache used to cache tile images. The default is MemoryCache.Default.
///
public static ObjectCache Cache = MemoryCache.Default;
///
/// Optional value to be used for the HttpWebRequest.UserAgent property. The default is null.
///
public static string HttpUserAgent;
private class PendingTile
{
public readonly Tile Tile;
public readonly ImageSource CachedImage;
public PendingTile(Tile tile, ImageSource cachedImage = null)
{
Tile = tile;
CachedImage = cachedImage;
}
}
private readonly ConcurrentQueue pendingTiles = new ConcurrentQueue();
private int taskCount;
public void BeginLoadTiles(TileLayer tileLayer, IEnumerable tiles)
{
if (tiles.Any())
{
// get current TileLayer property values in UI thread
var dispatcher = tileLayer.Dispatcher;
var tileSource = tileLayer.TileSource;
var imageTileSource = tileSource as ImageTileSource;
if (imageTileSource != null && !imageTileSource.IsAsync) // call LoadImage in UI thread with low priority
{
foreach (var tile in tiles)
{
dispatcher.BeginInvoke(new Action(t => t.SetImage(LoadImage(imageTileSource, t))), DispatcherPriority.Background, tile);
}
}
else
{
var tileList = tiles.ToList();
var sourceName = tileLayer.SourceName;
var maxDownloads = tileLayer.MaxParallelDownloads;
Task.Run(() => GetTiles(tileList, dispatcher, tileSource, sourceName, maxDownloads));
}
}
}
public void CancelLoadTiles(TileLayer tileLayer)
{
PendingTile pendingTile;
while (pendingTiles.TryDequeue(out pendingTile)) ; // no Clear method
}
private void GetTiles(List tiles, Dispatcher dispatcher, TileSource tileSource, string sourceName, int maxDownloads)
{
if (Cache != null &&
!string.IsNullOrWhiteSpace(sourceName) &&
!(tileSource is ImageTileSource) &&
!tileSource.UriFormat.StartsWith("file:"))
{
foreach (var tile in tiles)
{
BitmapSource image;
if (GetCachedImage(GetCacheKey(sourceName, tile), out image))
{
dispatcher.BeginInvoke(new Action((t, i) => t.SetImage(i)), tile, image);
}
else
{
pendingTiles.Enqueue(new PendingTile(tile, image));
}
}
}
else
{
foreach (var tile in tiles)
{
pendingTiles.Enqueue(new PendingTile(tile));
}
}
var newTaskCount = Math.Min(pendingTiles.Count, maxDownloads) - taskCount;
while (newTaskCount-- > 0)
{
Interlocked.Increment(ref taskCount);
Task.Run(() => LoadPendingTiles(dispatcher, tileSource, sourceName));
}
}
private void LoadPendingTiles(Dispatcher dispatcher, TileSource tileSource, string sourceName)
{
var imageTileSource = tileSource as ImageTileSource;
PendingTile pendingTile;
while (pendingTiles.TryDequeue(out pendingTile))
{
var tile = pendingTile.Tile;
ImageSource image = null;
if (imageTileSource != null)
{
image = LoadImage(imageTileSource, tile);
}
else
{
var uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
if (uri != null)
{
if (uri.Scheme == "file") // create from FileStream because creating from Uri leaves the file open
{
image = CreateImage(uri.LocalPath);
}
else
{
HttpStatusCode statusCode;
image = DownloadImage(uri, GetCacheKey(sourceName, tile), out statusCode);
if (statusCode == HttpStatusCode.NotFound)
{
tileSource.IgnoreTile(tile.XIndex, tile.Y, tile.ZoomLevel); // do not request again
}
else if (image == null) // download failed, use cached image if available
{
image = pendingTile.CachedImage;
}
}
}
}
if (image != null)
{
dispatcher.BeginInvoke(new Action((t, i) => t.SetImage(i)), tile, image);
}
else
{
tile.SetImage(null);
}
}
Interlocked.Decrement(ref taskCount);
}
private static ImageSource LoadImage(ImageTileSource tileSource, Tile tile)
{
ImageSource image = null;
try
{
image = tileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel);
}
catch (Exception ex)
{
Debug.WriteLine("Loading tile image failed: {0}", (object)ex.Message);
}
return image;
}
private static ImageSource CreateImage(string path)
{
ImageSource image = null;
if (File.Exists(path))
{
try
{
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
image = BitmapFrame.Create(fileStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
}
catch (Exception ex)
{
Debug.WriteLine("Creating tile image failed: {0}", (object)ex.Message);
}
}
return image;
}
private static ImageSource DownloadImage(Uri uri, string cacheKey, out HttpStatusCode statusCode)
{
BitmapSource image = null;
statusCode = HttpStatusCode.Unused;
try
{
var request = HttpWebRequest.CreateHttp(uri);
request.UserAgent = HttpUserAgent;
using (var response = (HttpWebResponse)request.GetResponse())
{
statusCode = response.StatusCode;
using (var responseStream = response.GetResponseStream())
using (var memoryStream = new MemoryStream())
{
responseStream.CopyTo(memoryStream);
memoryStream.Position = 0;
image = BitmapFrame.Create(memoryStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
if (cacheKey != null)
{
SetCachedImage(cacheKey, image, GetExpiration(response.Headers));
}
}
}
catch (WebException ex)
{
var response = ex.Response as HttpWebResponse;
if (response != null)
{
statusCode = response.StatusCode;
}
Debug.WriteLine("Downloading {0} failed: {1}: {2}", uri, ex.Status, ex.Message);
}
catch (Exception ex)
{
Debug.WriteLine("Downloading {0} failed: {1}", uri, ex.Message);
}
return image;
}
private static string GetCacheKey(string sourceName, Tile tile)
{
if (Cache == null || string.IsNullOrWhiteSpace(sourceName))
{
return null;
}
return string.Format("{0}/{1}/{2}/{3}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y);
}
private static bool GetCachedImage(string cacheKey, out BitmapSource image)
{
image = Cache.Get(cacheKey) as BitmapSource;
if (image == null)
{
return false;
}
var metadata = (BitmapMetadata)image.Metadata;
DateTime expiration;
// get cache expiration date from BitmapMetadata.DateTaken, must be parsed with CurrentCulture
return metadata == null
|| metadata.DateTaken == null
|| !DateTime.TryParse(metadata.DateTaken, CultureInfo.CurrentCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out expiration)
|| expiration > DateTime.UtcNow;
}
private static void SetCachedImage(string cacheKey, BitmapSource image, DateTime expiration)
{
var bitmap = BitmapFrame.Create(image);
var metadata = (BitmapMetadata)bitmap.Metadata;
// store cache expiration date in BitmapMetadata.DateTaken
metadata.DateTaken = expiration.ToString(CultureInfo.InvariantCulture);
metadata.Freeze();
bitmap.Freeze();
Cache.Set(cacheKey, bitmap, new CacheItemPolicy { AbsoluteExpiration = expiration });
//Debug.WriteLine("Cached {0}, Expires {1}", cacheKey, expiration);
}
private static DateTime GetExpiration(WebHeaderCollection headers)
{
var cacheControl = headers["Cache-Control"];
int maxAge;
DateTime expiration;
if (cacheControl != null &&
cacheControl.StartsWith("max-age=") &&
int.TryParse(cacheControl.Substring(8), out maxAge))
{
maxAge = Math.Min(maxAge, (int)DefaultCacheExpiration.TotalSeconds);
expiration = DateTime.UtcNow.AddSeconds(maxAge);
}
else
{
var expires = headers["Expires"];
var maxExpiration = DateTime.UtcNow.Add(DefaultCacheExpiration);
if (expires == null ||
!DateTime.TryParse(expires, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out expiration) ||
expiration > maxExpiration)
{
expiration = maxExpiration;
}
}
return expiration;
}
}
}