mirror of
https://github.com/ClemensFischer/XAML-Map-Control.git
synced 2025-12-06 07:12:04 +01:00
330 lines
12 KiB
C#
330 lines
12 KiB
C#
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
|
|
// © 2015 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
|
|
{
|
|
/// <summary>
|
|
/// Loads map tile images and optionally caches them in a System.Runtime.Caching.ObjectCache.
|
|
/// </summary>
|
|
public class TileImageLoader : ITileImageLoader
|
|
{
|
|
/// <summary>
|
|
/// Default name of an ObjectCache instance that is assigned to the Cache property.
|
|
/// </summary>
|
|
public const string DefaultCacheName = "TileCache";
|
|
|
|
/// <summary>
|
|
/// Default folder path where an ObjectCache instance may save cached data.
|
|
/// </summary>
|
|
public static readonly string DefaultCacheFolder =
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl");
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public static TimeSpan DefaultCacheExpiration = TimeSpan.FromDays(7);
|
|
|
|
/// <summary>
|
|
/// The ObjectCache used to cache tile images. The default is MemoryCache.Default.
|
|
/// </summary>
|
|
public static ObjectCache Cache = MemoryCache.Default;
|
|
|
|
/// <summary>
|
|
/// Optional value to be used for the HttpWebRequest.UserAgent property. The default is null.
|
|
/// </summary>
|
|
public static string HttpUserAgent;
|
|
|
|
private class PendingTile
|
|
{
|
|
public readonly Tile Tile;
|
|
public readonly ImageSource CachedImage;
|
|
|
|
public PendingTile(Tile tile, ImageSource cachedImage)
|
|
{
|
|
Tile = tile;
|
|
CachedImage = cachedImage;
|
|
}
|
|
}
|
|
|
|
private readonly ConcurrentQueue<PendingTile> pendingTiles = new ConcurrentQueue<PendingTile>();
|
|
private int taskCount;
|
|
|
|
public void BeginLoadTiles(TileLayer tileLayer, IEnumerable<Tile> 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<Tile>(t => t.SetImage(LoadImage(imageTileSource, t))), DispatcherPriority.Background, tile);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var tileList = tiles.ToList(); // evaluate immediately
|
|
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(IEnumerable<Tile> tiles, Dispatcher dispatcher, TileSource tileSource, string sourceName, int maxDownloads)
|
|
{
|
|
var useCache = Cache != null
|
|
&& !string.IsNullOrWhiteSpace(sourceName)
|
|
&& !(tileSource is ImageTileSource)
|
|
&& !tileSource.UriFormat.StartsWith("file:");
|
|
|
|
foreach (var tile in tiles)
|
|
{
|
|
BitmapSource cachedImage = null;
|
|
|
|
if (useCache && GetCachedImage(CacheKey(sourceName, tile), out cachedImage))
|
|
{
|
|
dispatcher.BeginInvoke(new Action<Tile, ImageSource>((t, i) => t.SetImage(i)), tile, cachedImage);
|
|
}
|
|
else
|
|
{
|
|
pendingTiles.Enqueue(new PendingTile(tile, cachedImage));
|
|
}
|
|
}
|
|
|
|
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") // load from FileStream as loading from Uri leaves file open
|
|
{
|
|
image = LoadImage(uri.LocalPath);
|
|
}
|
|
else
|
|
{
|
|
image = DownloadImage(uri, CacheKey(sourceName, tile))
|
|
?? pendingTile.CachedImage; // use possibly cached image if download failed
|
|
}
|
|
}
|
|
}
|
|
|
|
if (image != null)
|
|
{
|
|
dispatcher.BeginInvoke(new Action<Tile, ImageSource>((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 LoadImage(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)
|
|
{
|
|
BitmapSource image = null;
|
|
|
|
try
|
|
{
|
|
var request = HttpWebRequest.CreateHttp(uri);
|
|
request.UserAgent = HttpUserAgent;
|
|
|
|
using (var response = (HttpWebResponse)request.GetResponse())
|
|
{
|
|
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)
|
|
{
|
|
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 TileKey(TileSource tileSource, Tile tile)
|
|
{
|
|
return string.Format("{0:X}/{1:X}/{2:X}/{3:X}", tileSource.GetHashCode(), tile.ZoomLevel, tile.XIndex, tile.Y);
|
|
}
|
|
|
|
private static string CacheKey(string sourceName, Tile tile)
|
|
{
|
|
return string.IsNullOrEmpty(sourceName) ? null : 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;
|
|
}
|
|
}
|
|
}
|