2014-07-09 21:27:28 +02:00
|
|
|
|
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
|
2016-02-23 20:07:30 +01:00
|
|
|
|
// © 2016 Clemens Fischer
|
2014-07-09 21:27:28 +02:00
|
|
|
|
// Licensed under the Microsoft Public License (Ms-PL)
|
|
|
|
|
|
|
|
|
|
|
|
using System;
|
2014-11-19 21:11:14 +01:00
|
|
|
|
using System.Collections.Concurrent;
|
2014-07-09 21:27:28 +02:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using System.IO;
|
2014-11-19 21:11:14 +01:00
|
|
|
|
using System.Threading;
|
2014-07-09 21:27:28 +02:00
|
|
|
|
using System.Threading.Tasks;
|
2014-11-19 21:11:14 +01:00
|
|
|
|
using Windows.Storage;
|
2014-07-09 21:27:28 +02:00
|
|
|
|
using Windows.Storage.Streams;
|
|
|
|
|
|
using Windows.UI.Core;
|
|
|
|
|
|
using Windows.UI.Xaml.Media.Imaging;
|
|
|
|
|
|
using Windows.Web.Http;
|
|
|
|
|
|
using Windows.Web.Http.Filters;
|
|
|
|
|
|
|
|
|
|
|
|
namespace MapControl
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
2014-11-19 21:11:14 +01:00
|
|
|
|
/// Loads map tile images and optionally caches them in a IImageCache.
|
2014-07-09 21:27:28 +02:00
|
|
|
|
/// </summary>
|
2014-10-19 21:50:23 +02:00
|
|
|
|
public class TileImageLoader : ITileImageLoader
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2014-11-19 21:11:14 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Default name of an IImageCache instance that is assigned to the Cache property.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public const string DefaultCacheName = "TileCache";
|
2014-07-09 21:27:28 +02:00
|
|
|
|
|
2014-11-19 21:11:14 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Default StorageFolder where an IImageCache instance may save cached data.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static readonly StorageFolder DefaultCacheFolder = ApplicationData.Current.TemporaryFolder;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 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.
|
2014-11-19 21:11:14 +01:00
|
|
|
|
/// </summary>
|
2015-11-11 19:48:50 +01:00
|
|
|
|
public static TimeSpan DefaultCacheExpiration { get; set; }
|
2014-11-19 21:11:14 +01: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; }
|
|
|
|
|
|
|
2014-11-19 21:11:14 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// The IImageCache implementation used to cache tile images. The default is null.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static Caching.IImageCache Cache;
|
|
|
|
|
|
|
2015-11-11 19:48:50 +01:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2014-11-19 21:11:14 +01:00
|
|
|
|
private class PendingTile
|
|
|
|
|
|
{
|
|
|
|
|
|
public readonly Tile Tile;
|
|
|
|
|
|
public readonly Uri Uri;
|
|
|
|
|
|
|
|
|
|
|
|
public PendingTile(Tile tile, Uri uri)
|
|
|
|
|
|
{
|
|
|
|
|
|
Tile = tile;
|
|
|
|
|
|
Uri = uri;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private readonly ConcurrentQueue<PendingTile> pendingTiles = new ConcurrentQueue<PendingTile>();
|
|
|
|
|
|
private int taskCount;
|
2014-07-09 21:27:28 +02:00
|
|
|
|
|
2014-10-19 21:50:23 +02:00
|
|
|
|
public void BeginLoadTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2014-11-19 21:11:14 +01:00
|
|
|
|
var tileSource = tileLayer.TileSource;
|
|
|
|
|
|
var imageTileSource = tileSource as ImageTileSource;
|
2014-07-09 21:27:28 +02:00
|
|
|
|
|
2016-01-14 17:56:25 +01:00
|
|
|
|
if (imageTileSource != null)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2016-01-14 17:56:25 +01:00
|
|
|
|
foreach (var tile in tiles)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2016-01-14 17:56:25 +01:00
|
|
|
|
try
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2014-11-19 21:11:14 +01:00
|
|
|
|
tile.SetImage(imageTileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel));
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
2016-01-14 17:56:25 +01:00
|
|
|
|
catch (Exception ex)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2016-01-14 17:56:25 +01:00
|
|
|
|
Debug.WriteLine(ex.Message);
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2016-01-14 17:56:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var tile in tiles)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2016-01-14 17:56:25 +01:00
|
|
|
|
Uri uri = null;
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2016-01-14 17:56:25 +01:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.WriteLine(ex.Message);
|
|
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2016-01-14 17:56:25 +01:00
|
|
|
|
if (uri == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
tile.SetImage(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
pendingTiles.Enqueue(new PendingTile(tile, uri));
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2016-01-14 17:56:25 +01:00
|
|
|
|
var newTaskCount = Math.Min(pendingTiles.Count, tileLayer.MaxParallelDownloads) - taskCount;
|
|
|
|
|
|
var sourceName = tileLayer.SourceName;
|
|
|
|
|
|
|
|
|
|
|
|
while (newTaskCount-- > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
Interlocked.Increment(ref taskCount);
|
|
|
|
|
|
|
2016-06-04 08:42:38 +02:00
|
|
|
|
Task.Run(() => LoadPendingTiles(tileSource, sourceName)); // Task.Run(Func<Task>)
|
2016-01-14 17:56:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2014-10-19 21:50:23 +02:00
|
|
|
|
public void CancelLoadTiles(TileLayer tileLayer)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2014-11-19 21:11:14 +01:00
|
|
|
|
PendingTile pendingTile;
|
|
|
|
|
|
|
|
|
|
|
|
while (pendingTiles.TryDequeue(out pendingTile)) ; // no Clear method
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2014-11-19 21:11:14 +01:00
|
|
|
|
private async Task LoadPendingTiles(TileSource tileSource, string sourceName)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2014-11-19 21:11:14 +01:00
|
|
|
|
PendingTile pendingTile;
|
2014-07-09 21:27:28 +02:00
|
|
|
|
|
2016-09-29 20:47:31 +02:00
|
|
|
|
while (pendingTiles.TryDequeue(out pendingTile))
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2016-09-29 20:47:31 +02:00
|
|
|
|
var tile = pendingTile.Tile;
|
|
|
|
|
|
var uri = pendingTile.Uri;
|
|
|
|
|
|
|
|
|
|
|
|
if (Cache == null || sourceName == null)
|
2014-11-19 21:11:14 +01:00
|
|
|
|
{
|
2016-09-29 20:47:31 +02:00
|
|
|
|
await DownloadImage(tile, uri, null);
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
2016-09-29 20:47:31 +02:00
|
|
|
|
else
|
2016-01-14 17:56:25 +01:00
|
|
|
|
{
|
|
|
|
|
|
var extension = Path.GetExtension(uri.LocalPath);
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2016-01-14 17:56:25 +01:00
|
|
|
|
if (string.IsNullOrEmpty(extension) || extension == ".jpeg")
|
|
|
|
|
|
{
|
|
|
|
|
|
extension = ".jpg";
|
|
|
|
|
|
}
|
2014-07-09 21:27:28 +02:00
|
|
|
|
|
2016-01-14 17:56:25 +01:00
|
|
|
|
var cacheKey = string.Format(@"{0}\{1}\{2}\{3}{4}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y, extension);
|
2016-09-29 20:47:31 +02:00
|
|
|
|
var cacheItem = await Cache.GetAsync(cacheKey);
|
2016-01-14 17:56:25 +01:00
|
|
|
|
var loaded = false;
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2016-01-14 17:56:25 +01:00
|
|
|
|
if (cacheItem == null || cacheItem.Expiration <= DateTime.UtcNow)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2016-09-29 20:47:31 +02:00
|
|
|
|
loaded = await DownloadImage(tile, uri, cacheKey);
|
2016-01-14 17:56:25 +01:00
|
|
|
|
}
|
2015-01-20 17:52:02 +01:00
|
|
|
|
|
2016-01-14 17:56:25 +01:00
|
|
|
|
if (!loaded && cacheItem != null && cacheItem.Buffer != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
using (var stream = new InMemoryRandomAccessStream())
|
|
|
|
|
|
{
|
|
|
|
|
|
await stream.WriteAsync(cacheItem.Buffer);
|
|
|
|
|
|
await stream.FlushAsync();
|
|
|
|
|
|
stream.Seek(0);
|
|
|
|
|
|
|
2016-09-29 20:47:31 +02:00
|
|
|
|
await LoadImageFromStream(tile, stream);
|
2016-01-14 17:56:25 +01:00
|
|
|
|
}
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
|
|
|
|
|
Interlocked.Decrement(ref taskCount);
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-09-29 20:47:31 +02:00
|
|
|
|
private async Task<bool> DownloadImage(Tile tile, Uri uri, string cacheKey)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2014-11-19 21:11:14 +01:00
|
|
|
|
using (var httpClient = new HttpClient(new HttpBaseProtocolFilter { AllowAutoRedirect = false }))
|
|
|
|
|
|
using (var response = await httpClient.GetAsync(uri))
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2014-11-19 21:11:14 +01:00
|
|
|
|
if (response.IsSuccessStatusCode)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2016-09-29 20:47:31 +02:00
|
|
|
|
return await LoadImageFromHttpResponse(response, tile, cacheKey);
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2015-12-03 20:04:10 +01:00
|
|
|
|
Debug.WriteLine("{0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase);
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.WriteLine("{0}: {1}", uri, ex.Message);
|
|
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2015-01-20 17:52:02 +01:00
|
|
|
|
return false;
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-09-29 20:47:31 +02:00
|
|
|
|
private async Task<bool> LoadImageFromHttpResponse(HttpResponseMessage response, Tile tile, string cacheKey)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2016-09-29 20:47:31 +02:00
|
|
|
|
string tileInfo;
|
|
|
|
|
|
|
|
|
|
|
|
if (response.Headers.TryGetValue("X-VE-Tile-Info", out tileInfo) && tileInfo == "no-tile") // set by Bing Maps
|
|
|
|
|
|
{
|
|
|
|
|
|
tile.SetImage(null);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2014-11-19 21:11:14 +01:00
|
|
|
|
using (var stream = new InMemoryRandomAccessStream())
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
|
|
|
|
|
using (var content = response.Content)
|
|
|
|
|
|
{
|
|
|
|
|
|
await content.WriteToStreamAsync(stream);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await stream.FlushAsync();
|
|
|
|
|
|
stream.Seek(0);
|
|
|
|
|
|
|
2016-09-29 20:47:31 +02:00
|
|
|
|
var loaded = await LoadImageFromStream(tile, stream);
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2016-09-29 20:47:31 +02:00
|
|
|
|
if (loaded && cacheKey != null)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2014-11-19 21:11:14 +01:00
|
|
|
|
var buffer = new Windows.Storage.Streams.Buffer((uint)stream.Size);
|
|
|
|
|
|
|
|
|
|
|
|
stream.Seek(0);
|
|
|
|
|
|
await stream.ReadAsync(buffer, buffer.Capacity, InputStreamOptions.None);
|
|
|
|
|
|
|
2015-12-03 20:04:10 +01:00
|
|
|
|
var expiration = DefaultCacheExpiration;
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
2015-12-03 20:04:10 +01:00
|
|
|
|
if (response.Headers.CacheControl.MaxAge.HasValue)
|
2014-07-09 21:27:28 +02:00
|
|
|
|
{
|
2015-12-03 20:04:10 +01:00
|
|
|
|
expiration = response.Headers.CacheControl.MaxAge.Value;
|
|
|
|
|
|
|
|
|
|
|
|
if (expiration < MinimumCacheExpiration)
|
|
|
|
|
|
{
|
|
|
|
|
|
expiration = MinimumCacheExpiration;
|
|
|
|
|
|
}
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
2014-07-09 21:27:28 +02:00
|
|
|
|
|
2016-09-29 20:47:31 +02:00
|
|
|
|
await Cache.SetAsync(cacheKey, buffer, DateTime.UtcNow.Add(expiration));
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
2014-07-09 21:27:28 +02:00
|
|
|
|
|
2015-01-20 17:52:02 +01:00
|
|
|
|
return loaded;
|
2014-11-19 21:11:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2014-07-09 21:27:28 +02:00
|
|
|
|
|
2016-09-29 20:47:31 +02:00
|
|
|
|
private async Task<bool> LoadImageFromStream(Tile tile, IRandomAccessStream stream)
|
2014-11-19 21:11:14 +01:00
|
|
|
|
{
|
|
|
|
|
|
var completion = new TaskCompletionSource<bool>();
|
|
|
|
|
|
|
2016-09-29 20:47:31 +02:00
|
|
|
|
var action = tile.Image.Dispatcher.RunAsync(
|
2014-11-19 21:11:14 +01:00
|
|
|
|
CoreDispatcherPriority.Normal,
|
|
|
|
|
|
async () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2016-09-29 20:47:31 +02:00
|
|
|
|
var image = new BitmapImage();
|
2014-11-19 21:11:14 +01:00
|
|
|
|
await image.SetSourceAsync(stream);
|
|
|
|
|
|
tile.SetImage(image, true, false);
|
|
|
|
|
|
completion.SetResult(true);
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2014-11-19 21:11:14 +01:00
|
|
|
|
Debug.WriteLine(ex.Message);
|
|
|
|
|
|
tile.SetImage(null);
|
|
|
|
|
|
completion.SetResult(false);
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2014-11-19 21:11:14 +01:00
|
|
|
|
|
|
|
|
|
|
return await completion.Task;
|
2014-07-09 21:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|