2017-06-25 23:05:48 +02:00
|
|
|
|
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
|
|
|
|
|
|
// © 2017 Clemens Fischer
|
2012-05-04 12:52:20 +02:00
|
|
|
|
// 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;
|
2015-11-28 21:09:25 +01:00
|
|
|
|
using System.Text;
|
2012-04-25 22:02:53 +02:00
|
|
|
|
using System.Threading;
|
2014-11-19 21:11:14 +01:00
|
|
|
|
using System.Threading.Tasks;
|
2012-11-07 17:48:15 +01:00
|
|
|
|
using System.Windows.Media;
|
2012-04-25 22:02:53 +02:00
|
|
|
|
|
|
|
|
|
|
namespace MapControl
|
|
|
|
|
|
{
|
2012-05-04 12:52:20 +02:00
|
|
|
|
/// <summary>
|
2013-11-12 21:14:53 +01:00
|
|
|
|
/// Loads map tile images and optionally caches them in a System.Runtime.Caching.ObjectCache.
|
2012-05-04 12:52:20 +02:00
|
|
|
|
/// </summary>
|
2014-10-19 21:50:23 +02:00
|
|
|
|
public class TileImageLoader : ITileImageLoader
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2012-08-15 21:31:10 +02:00
|
|
|
|
/// <summary>
|
2014-11-19 21:11:14 +01:00
|
|
|
|
/// Default name of an ObjectCache instance that is assigned to the Cache property.
|
2012-08-15 21:31:10 +02:00
|
|
|
|
/// </summary>
|
2013-11-12 21:14:53 +01:00
|
|
|
|
public const string DefaultCacheName = "TileCache";
|
2012-08-15 21:31:10 +02:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2014-11-19 21:11:14 +01:00
|
|
|
|
/// Default folder path where an ObjectCache instance may save cached data.
|
2012-08-15 21:31:10 +02:00
|
|
|
|
/// </summary>
|
2014-11-19 21:11:14 +01:00
|
|
|
|
public static readonly string DefaultCacheFolder =
|
2012-08-15 21:31:10 +02:00
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl");
|
|
|
|
|
|
|
2012-07-03 18:03:56 +02:00
|
|
|
|
/// <summary>
|
2014-11-19 21:11:14 +01:00
|
|
|
|
/// Default expiration time for cached tile images. Used when no expiration time
|
2015-12-03 20:04:10 +01:00
|
|
|
|
/// was transmitted on download. The default value is one day.
|
2012-07-03 18:03:56 +02:00
|
|
|
|
/// </summary>
|
2015-11-11 19:48:50 +01:00
|
|
|
|
public static TimeSpan DefaultCacheExpiration { get; set; }
|
2012-07-03 18:03:56 +02:00
|
|
|
|
|
2015-12-03 20:04:10 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Minimum expiration time for cached tile images. Used when an unnecessarily small expiration time
|
|
|
|
|
|
/// was transmitted on download (e.g. Cache-Control: max-age=0). The default value is one hour.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static TimeSpan MinimumCacheExpiration { get; set; }
|
|
|
|
|
|
|
2012-07-03 18:03:56 +02:00
|
|
|
|
/// <summary>
|
2014-10-19 21:50:23 +02:00
|
|
|
|
/// The ObjectCache used to cache tile images. The default is MemoryCache.Default.
|
2012-07-03 18:03:56 +02:00
|
|
|
|
/// </summary>
|
2015-11-11 19:48:50 +01:00
|
|
|
|
public static ObjectCache Cache { get; set; }
|
2012-07-03 18:03:56 +02:00
|
|
|
|
|
2014-11-19 21:11:14 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Optional value to be used for the HttpWebRequest.UserAgent property. The default is null.
|
|
|
|
|
|
/// </summary>
|
2015-11-11 19:48:50 +01:00
|
|
|
|
public static string HttpUserAgent { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
static TileImageLoader()
|
|
|
|
|
|
{
|
2015-12-03 20:04:10 +01:00
|
|
|
|
DefaultCacheExpiration = TimeSpan.FromDays(1);
|
|
|
|
|
|
MinimumCacheExpiration = TimeSpan.FromHours(1);
|
2015-11-11 19:48:50 +01:00
|
|
|
|
Cache = MemoryCache.Default;
|
|
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
private readonly ConcurrentStack<Tile> pendingTiles = new ConcurrentStack<Tile>();
|
2014-11-19 21:11:14 +01:00
|
|
|
|
private int taskCount;
|
2012-04-25 22:02:53 +02:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
public void LoadTiles(MapTileLayer tileLayer)
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
pendingTiles.Clear();
|
|
|
|
|
|
|
|
|
|
|
|
var tileStack = tileLayer.Tiles.Where(t => t.Pending).Reverse().ToArray();
|
|
|
|
|
|
|
|
|
|
|
|
if (tileStack.Length > 0)
|
2013-08-29 15:49:48 +02:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
pendingTiles.PushRange(tileStack);
|
|
|
|
|
|
|
2013-11-12 21:14:53 +01:00
|
|
|
|
var tileSource = tileLayer.TileSource;
|
2017-07-17 21:31:09 +02:00
|
|
|
|
var sourceName = tileLayer.SourceName;
|
|
|
|
|
|
var maxDownloads = tileLayer.MaxParallelDownloads;
|
2013-11-12 21:14:53 +01:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
while (taskCount < Math.Min(pendingTiles.Count, maxDownloads))
|
2014-07-01 18:57:44 +02:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
Interlocked.Increment(ref taskCount);
|
|
|
|
|
|
|
|
|
|
|
|
Task.Run(() =>
|
2014-07-01 18:57:44 +02:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
LoadPendingTiles(tileSource, sourceName);
|
2014-07-01 18:57:44 +02:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
Interlocked.Decrement(ref taskCount);
|
|
|
|
|
|
});
|
2014-07-01 18:57:44 +02:00
|
|
|
|
}
|
2013-08-29 15:49:48 +02:00
|
|
|
|
}
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
private void LoadPendingTiles(TileSource tileSource, string sourceName)
|
2013-01-17 18:48:38 +01:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
var imageTileSource = tileSource as ImageTileSource;
|
|
|
|
|
|
Tile tile;
|
2015-01-20 17:52:02 +01:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
while (pendingTiles.TryPop(out tile))
|
2012-07-20 21:57:29 +02:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
tile.Pending = false;
|
2013-11-21 21:16:29 +01:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
try
|
2015-01-20 17:52:02 +01:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
ImageSource image = null;
|
|
|
|
|
|
Uri uri;
|
|
|
|
|
|
|
|
|
|
|
|
if (imageTileSource != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
image = imageTileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if ((uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel)) != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
image = LoadImage(uri, sourceName, tile.XIndex, tile.Y, tile.ZoomLevel);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (image != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
tile.SetImage(image);
|
|
|
|
|
|
}
|
2013-11-21 21:16:29 +01:00
|
|
|
|
}
|
2017-07-17 21:31:09 +02:00
|
|
|
|
catch (Exception ex)
|
2014-11-19 21:11:14 +01:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
Debug.WriteLine("{0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message);
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
2013-11-12 21:14:53 +01:00
|
|
|
|
}
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
private ImageSource LoadImage(Uri uri, string sourceName, int x, int y, int zoomLevel)
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
ImageSource image = null;
|
2013-04-04 18:01:13 +02:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
try
|
2012-04-25 22:02:53 +02:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
if (!uri.IsAbsoluteUri)
|
|
|
|
|
|
{
|
|
|
|
|
|
image = BitmapSourceHelper.FromFile(uri.OriginalString);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (uri.Scheme == "file")
|
|
|
|
|
|
{
|
|
|
|
|
|
image = BitmapSourceHelper.FromFile(uri.LocalPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (Cache == null || string.IsNullOrEmpty(sourceName))
|
2012-07-20 21:57:29 +02:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
image = DownloadImage(uri, null);
|
2013-01-17 18:48:38 +01:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
var cacheKey = string.Format("{0}/{1}/{2}/{3}", sourceName, zoomLevel, x, y);
|
2013-01-17 18:48:38 +01:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
if (!GetCachedImage(cacheKey, ref image))
|
2013-01-17 18:48:38 +01:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
// Either no cached image was found or expiration time has expired.
|
|
|
|
|
|
// If download fails use possibly cached but expired image anyway.
|
|
|
|
|
|
image = DownloadImage(uri, cacheKey);
|
2013-01-17 18:48:38 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
2017-07-17 21:31:09 +02:00
|
|
|
|
catch (WebException ex)
|
2013-11-12 21:14:53 +01:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
Debug.WriteLine("{0}: {1}: {2}", uri, ex.Status, ex.Message);
|
2013-11-12 21:14:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
Debug.WriteLine("{0}: {1}", uri, ex.Message);
|
2013-11-12 21:14:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return image;
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
private static ImageSource DownloadImage(Uri uri, string cacheKey)
|
2013-11-12 21:14:53 +01:00
|
|
|
|
{
|
|
|
|
|
|
ImageSource image = null;
|
2017-07-17 21:31:09 +02:00
|
|
|
|
var request = WebRequest.CreateHttp(uri);
|
2013-11-12 21:14:53 +01:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
if (HttpUserAgent != null)
|
2013-11-12 21:14:53 +01:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
request.UserAgent = HttpUserAgent;
|
2012-08-24 18:10:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
using (var response = (HttpWebResponse)request.GetResponse())
|
2012-08-24 18:10:12 +02:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
if (response.Headers["X-VE-Tile-Info"] != "no-tile") // set by Bing Maps
|
2015-11-28 21:09:25 +01:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
using (var responseStream = response.GetResponseStream())
|
|
|
|
|
|
using (var memoryStream = new MemoryStream())
|
2014-10-19 21:50:23 +02:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
responseStream.CopyTo(memoryStream);
|
|
|
|
|
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
|
|
|
|
|
image = BitmapSourceHelper.FromStream(memoryStream);
|
|
|
|
|
|
|
|
|
|
|
|
if (cacheKey != null)
|
2015-11-28 21:09:25 +01:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
SetCachedImage(cacheKey, memoryStream, GetExpiration(response.Headers));
|
2015-11-28 21:09:25 +01:00
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2014-11-19 21:11:14 +01:00
|
|
|
|
return image;
|
2012-07-03 18:03:56 +02:00
|
|
|
|
}
|
2014-07-01 18:57:44 +02:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
private static bool GetCachedImage(string cacheKey, ref ImageSource image)
|
2015-01-20 17:52:02 +01:00
|
|
|
|
{
|
2017-07-17 21:31:09 +02:00
|
|
|
|
var result = false;
|
2015-11-28 21:09:25 +01:00
|
|
|
|
var buffer = Cache.Get(cacheKey) as byte[];
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2015-11-28 21:09:25 +01:00
|
|
|
|
if (buffer != null)
|
2014-07-01 18:57:44 +02:00
|
|
|
|
{
|
2015-11-28 21:09:25 +01:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using (var memoryStream = new MemoryStream(buffer))
|
|
|
|
|
|
{
|
2017-06-25 23:05:48 +02:00
|
|
|
|
image = BitmapSourceHelper.FromStream(memoryStream);
|
2015-11-28 21:09:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DateTime expiration = DateTime.MinValue;
|
|
|
|
|
|
|
|
|
|
|
|
if (buffer.Length >= 16 && Encoding.ASCII.GetString(buffer, buffer.Length - 16, 8) == "EXPIRES:")
|
|
|
|
|
|
{
|
|
|
|
|
|
expiration = new DateTime(BitConverter.ToInt64(buffer, buffer.Length - 8), DateTimeKind.Utc);
|
|
|
|
|
|
}
|
2014-07-01 18:57:44 +02:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
result = expiration > DateTime.UtcNow;
|
2015-11-28 21:09:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.WriteLine("{0}: {1}", cacheKey, ex.Message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2017-07-17 21:31:09 +02:00
|
|
|
|
return result;
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2015-11-28 21:09:25 +01:00
|
|
|
|
private static void SetCachedImage(string cacheKey, MemoryStream memoryStream, DateTime expiration)
|
2014-11-19 21:11:14 +01:00
|
|
|
|
{
|
2015-11-28 21:09:25 +01:00
|
|
|
|
memoryStream.Seek(0, SeekOrigin.End);
|
|
|
|
|
|
memoryStream.Write(Encoding.ASCII.GetBytes("EXPIRES:"), 0, 8);
|
|
|
|
|
|
memoryStream.Write(BitConverter.GetBytes(expiration.Ticks), 0, 8);
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2015-11-28 21:09:25 +01:00
|
|
|
|
Cache.Set(cacheKey, memoryStream.ToArray(), new CacheItemPolicy { AbsoluteExpiration = expiration });
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static DateTime GetExpiration(WebHeaderCollection headers)
|
|
|
|
|
|
{
|
2015-12-03 20:04:10 +01:00
|
|
|
|
var expiration = DefaultCacheExpiration;
|
2014-11-19 21:11:14 +01:00
|
|
|
|
var cacheControl = headers["Cache-Control"];
|
|
|
|
|
|
|
2015-12-03 20:04:10 +01:00
|
|
|
|
if (cacheControl != null)
|
2014-07-01 18:57:44 +02:00
|
|
|
|
{
|
2015-12-03 20:04:10 +01:00
|
|
|
|
int maxAgeValue;
|
|
|
|
|
|
var maxAgeDirective = cacheControl
|
|
|
|
|
|
.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
|
|
|
|
|
.FirstOrDefault(s => s.StartsWith("max-age="));
|
2014-07-01 18:57:44 +02:00
|
|
|
|
|
2015-12-03 20:04:10 +01:00
|
|
|
|
if (maxAgeDirective != null &&
|
|
|
|
|
|
int.TryParse(maxAgeDirective.Substring(8), out maxAgeValue))
|
2014-11-19 21:11:14 +01:00
|
|
|
|
{
|
2015-12-03 20:04:10 +01:00
|
|
|
|
expiration = TimeSpan.FromSeconds(maxAgeValue);
|
|
|
|
|
|
|
|
|
|
|
|
if (expiration < MinimumCacheExpiration)
|
|
|
|
|
|
{
|
|
|
|
|
|
expiration = MinimumCacheExpiration;
|
|
|
|
|
|
}
|
2014-07-01 18:57:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2015-12-03 20:04:10 +01:00
|
|
|
|
return DateTime.UtcNow.Add(expiration);
|
2014-07-01 18:57:44 +02:00
|
|
|
|
}
|
2012-04-25 22:02:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|