// 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.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 value for the directory where an ObjectCache instance may save cached data. /// public static readonly string DefaultCacheDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl"); /// /// Default expiration time span for cached images. Used when no expiration date /// was transmitted on download. The default value is seven days. /// public static TimeSpan DefaultCacheExpiration { get; set; } /// /// The ObjectCache used to cache tile images. The default is MemoryCache.Default. /// public static ObjectCache Cache { get; set; } static TileImageLoader() { DefaultCacheExpiration = TimeSpan.FromDays(7); Cache = MemoryCache.Default; } private readonly ConcurrentQueue pendingTiles = new ConcurrentQueue(); private int threadCount; public void BeginLoadTiles(TileLayer tileLayer, IEnumerable tiles) { if (tiles.Any()) { // get current TileLayer property values in UI thread var tileSource = tileLayer.TileSource; var imageTileSource = tileSource as ImageTileSource; var animateOpacity = tileLayer.AnimateTileOpacity; var dispatcher = tileLayer.Dispatcher; if (imageTileSource != null && !imageTileSource.IsAsync) // call LoadImage in UI thread { var setImageAction = new Action(t => t.SetImageSource(LoadImage(imageTileSource, t), animateOpacity)); foreach (var tile in tiles) { dispatcher.BeginInvoke(setImageAction, DispatcherPriority.Background, tile); // with low priority } } else { var tileList = tiles.ToList(); var sourceName = tileLayer.SourceName; var maxDownloads = tileLayer.MaxParallelDownloads; ThreadPool.QueueUserWorkItem(o => GetTiles(tileList, dispatcher, tileSource, sourceName, animateOpacity, maxDownloads)); } } } public void CancelLoadTiles(TileLayer tileLayer) { Tile tile; while (pendingTiles.TryDequeue(out tile)) ; // no Clear method } private void GetTiles(List tiles, Dispatcher dispatcher, TileSource tileSource, string sourceName, bool animateOpacity, int maxDownloads) { if (Cache != null && !string.IsNullOrWhiteSpace(sourceName) && !(tileSource is ImageTileSource) && !tileSource.UriFormat.StartsWith("file:")) { var setImageAction = new Action((t, i) => t.SetImageSource(i, animateOpacity)); var outdatedTiles = new List(tiles.Count); foreach (var tile in tiles) { var buffer = Cache.Get(TileCache.Key(sourceName, tile)) as byte[]; var image = CreateImage(buffer); if (image == null) { pendingTiles.Enqueue(tile); // not yet cached } else if (TileCache.IsExpired(buffer)) { dispatcher.Invoke(setImageAction, tile, image); // synchronously before enqueuing outdatedTiles.Add(tile); // update outdated cache } else { dispatcher.BeginInvoke(setImageAction, tile, image); } } tiles = outdatedTiles; // enqueue outdated tiles after current tiles } foreach (var tile in tiles) { pendingTiles.Enqueue(tile); } while (threadCount < Math.Min(pendingTiles.Count, maxDownloads)) { Interlocked.Increment(ref threadCount); ThreadPool.QueueUserWorkItem(o => LoadPendingTiles(dispatcher, tileSource, sourceName, animateOpacity)); } } private void LoadPendingTiles(Dispatcher dispatcher, TileSource tileSource, string sourceName, bool animateOpacity) { var setImageAction = new Action((t, i) => t.SetImageSource(i, animateOpacity)); var imageTileSource = tileSource as ImageTileSource; Tile tile; while (pendingTiles.TryDequeue(out 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 { DateTime expirationTime; var buffer = DownloadImage(uri, out expirationTime); image = CreateImage(buffer); if (image != null && Cache != null && !string.IsNullOrWhiteSpace(sourceName) && expirationTime > DateTime.UtcNow) { Cache.Set(TileCache.Key(sourceName, tile), buffer, new CacheItemPolicy { AbsoluteExpiration = expirationTime }); } } } } if (image != null || !tile.HasImageSource) // set null image if tile does not yet have an ImageSource { dispatcher.BeginInvoke(setImageAction, tile, image); } } Interlocked.Decrement(ref threadCount); } private static ImageSource LoadImage(ImageTileSource tileSource, Tile tile) { ImageSource image = null; try { image = tileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel); } catch (Exception ex) { Trace.TraceWarning("Loading tile image failed: {0}", ex.Message); } return image; } private static ImageSource CreateImage(string path) { ImageSource image = null; if (File.Exists(path)) { try { using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { image = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); } } catch (Exception ex) { Trace.TraceWarning("Creating tile image failed: {0}", ex.Message); } } return image; } private static ImageSource CreateImage(byte[] buffer) { ImageSource image = null; if (buffer != null) { try { using (var stream = TileCache.ImageStream(buffer)) { image = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); } } catch (Exception ex) { Trace.TraceWarning("Creating tile image failed: {0}", ex.Message); } } return image; } private static byte[] DownloadImage(Uri uri, out DateTime expirationTime) { expirationTime = DateTime.UtcNow + DefaultCacheExpiration; byte[] buffer = null; try { var request = HttpWebRequest.CreateHttp(uri); using (var response = (HttpWebResponse)request.GetResponse()) using (var responseStream = response.GetResponseStream()) { var expiresHeader = response.Headers["Expires"]; DateTime expires; if (expiresHeader != null && DateTime.TryParse(expiresHeader, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out expires) && expirationTime > expires) { expirationTime = expires; } buffer = TileCache.CreateBuffer(responseStream, (int)response.ContentLength, expirationTime); } //Trace.TraceInformation("Downloaded {0}, expires {1}", uri, expirationTime); } catch (WebException ex) { if (ex.Status == WebExceptionStatus.ProtocolError) { var statusCode = ((HttpWebResponse)ex.Response).StatusCode; if (statusCode != HttpStatusCode.NotFound) { Trace.TraceWarning("Downloading {0} failed: {1}", uri, ex.Message); } } else { Trace.TraceWarning("Downloading {0} failed with {1}: {2}", uri, ex.Status, ex.Message); } } catch (Exception ex) { Trace.TraceWarning("Downloading {0} failed: {1}", uri, ex.Message); } return buffer; } private static class TileCache { private const int imageBufferOffset = sizeof(Int64); public static string Key(string sourceName, Tile tile) { return string.Format("{0}/{1}/{2}/{3}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y); } public static MemoryStream ImageStream(byte[] cacheBuffer) { return new MemoryStream(cacheBuffer, imageBufferOffset, cacheBuffer.Length - imageBufferOffset, false); } public static bool IsExpired(byte[] cacheBuffer) { return DateTime.FromBinary(BitConverter.ToInt64(cacheBuffer, 0)) < DateTime.UtcNow; } public static byte[] CreateBuffer(Stream imageStream, int length, DateTime expirationTime) { using (var memoryStream = length > 0 ? new MemoryStream(length + imageBufferOffset) : new MemoryStream()) { memoryStream.Write(BitConverter.GetBytes(expirationTime.ToBinary()), 0, imageBufferOffset); imageStream.CopyTo(memoryStream); return length > 0 ? memoryStream.GetBuffer() : memoryStream.ToArray(); } } } } }