Version 3.2. Improved TileImageLoader.

This commit is contained in:
ClemensF 2017-07-17 21:31:09 +02:00
parent 2ed22e59b2
commit 6938c8251d
25 changed files with 338 additions and 450 deletions

View file

@ -7,8 +7,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("© 2017 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("3.1.0")]
[assembly: AssemblyFileVersion("3.1.0")]
[assembly: AssemblyVersion("3.2.0")]
[assembly: AssemblyFileVersion("3.2.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

View file

@ -7,8 +7,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("© 2017 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("3.1.0")]
[assembly: AssemblyFileVersion("3.1.0")]
[assembly: AssemblyVersion("3.2.0")]
[assembly: AssemblyFileVersion("3.2.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

View file

@ -7,8 +7,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("© 2017 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("3.1.0")]
[assembly: AssemblyFileVersion("3.1.0")]
[assembly: AssemblyVersion("3.2.0")]
[assembly: AssemblyFileVersion("3.2.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

View file

@ -7,8 +7,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("© 2017 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("3.1.0")]
[assembly: AssemblyFileVersion("3.1.0")]
[assembly: AssemblyVersion("3.2.0")]
[assembly: AssemblyFileVersion("3.2.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

View file

@ -3,7 +3,6 @@
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Windows.Media.Imaging;
@ -11,43 +10,49 @@ using System.Windows.Media.Imaging;
namespace MapControl
{
/// <summary>
/// Creates frozen BitmapSources from Stream or Uri.
/// Creates frozen BitmapSources from Stream, file or Uri.
/// </summary>
public static class BitmapSourceHelper
{
public static BitmapSource FromStream(Stream stream)
{
var bitmap = new BitmapImage();
return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
public static BitmapSource FromFile(string path)
{
if (!File.Exists(path))
{
return null;
}
return bitmap;
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
{
return FromStream(fileStream);
}
}
public static BitmapSource FromUri(Uri uri)
{
try
if (!uri.IsAbsoluteUri)
{
using (var response = WebRequest.Create(uri).GetResponse())
using (var responseStream = response.GetResponseStream())
using (var memoryStream = new MemoryStream())
{
responseStream.CopyTo(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
return FromStream(memoryStream);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return FromFile(uri.OriginalString);
}
return null;
if (uri.Scheme == "file")
{
return FromFile(uri.LocalPath);
}
using (var response = WebRequest.Create(uri).GetResponse())
using (var responseStream = response.GetResponseStream())
using (var memoryStream = new MemoryStream())
{
responseStream.CopyTo(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
return FromStream(memoryStream);
}
}
}
}

View file

@ -3,40 +3,22 @@
// Licensed under the Microsoft Public License (Ms-PL)
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace MapControl
{
/// <summary>
/// Provides the image of a map tile. ImageTileSource bypasses image downloading
/// and optional caching in TileImageLoader. By overriding the LoadImage method,
/// an application can provide tile images from an arbitrary source.
/// If the IsAsync property is true, LoadImage will be called from a separate,
/// non-UI thread and must therefore return a frozen ImageSource.
/// Provides the image of a map tile.
/// ImageTileSource bypasses image downloading and optional caching in TileImageLoader.
/// By overriding the LoadImage method, an application can provide tile images from an arbitrary source.
/// LoadImage will be called from a non-UI thread and must therefore return a frozen ImageSource.
/// </summary>
public class ImageTileSource : TileSource
{
public bool IsAsync { get; set; }
public virtual ImageSource LoadImage(int x, int y, int zoomLevel)
{
ImageSource image = null;
var uri = GetUri(x, y, zoomLevel);
if (uri != null)
{
if (IsAsync)
{
image = BitmapSourceHelper.FromUri(uri);
}
else
{
image = new BitmapImage(uri);
}
}
return image;
return uri != null ? BitmapSourceHelper.FromUri(uri) : null;
}
}
}

View file

@ -3,6 +3,7 @@
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
@ -16,7 +17,17 @@ namespace MapControl
{
Task.Run(() =>
{
var image = BitmapSourceHelper.FromUri(uri);
BitmapSource image = null;
try
{
image = BitmapSourceHelper.FromUri(uri);
}
catch (Exception ex)
{
Debug.WriteLine("{0}: {1}", uri, ex.Message);
}
Dispatcher.BeginInvoke(new Action(() => UpdateImage(image)));
});
}

View file

@ -123,7 +123,7 @@ namespace MapControl
/// <summary>
/// Relative size of the map image in relation to the current viewport size.
/// Setting a value greater than one will let ImageLayer request images that
/// Setting a value greater than one will let MapImageLayer request images that
/// are larger than the viewport, in order to support smooth panning.
/// </summary>
public double RelativeImageSize
@ -151,7 +151,7 @@ namespace MapControl
}
/// <summary>
/// Description of the ImageLayer.
/// Description of the MapImageLayer.
/// Used to display copyright information on top of the map.
/// </summary>
public string Description
@ -162,7 +162,7 @@ namespace MapControl
/// <summary>
/// Optional foreground brush.
/// Sets MapBase.Foreground if not null and the ImageLayer is the base map layer.
/// Sets MapBase.Foreground if not null and the MapImageLayer is the base map layer.
/// </summary>
public Brush MapForeground
{
@ -172,7 +172,7 @@ namespace MapControl
/// <summary>
/// Optional background brush.
/// Sets MapBase.Background if not null and the ImageLayer is the base map layer.
/// Sets MapBase.Background if not null and the MapImageLayer is the base map layer.
/// </summary>
public Brush MapBackground
{

View file

@ -23,8 +23,7 @@ namespace MapControl
{
public interface ITileImageLoader
{
void BeginLoadTiles(MapTileLayer tileLayer, IEnumerable<Tile> tiles);
void CancelLoadTiles(MapTileLayer tileLayer);
void LoadTiles(MapTileLayer tileLayer);
}
/// <summary>
@ -38,7 +37,7 @@ namespace MapControl
public partial class MapTileLayer : Panel, IMapLayer
{
/// <summary>
/// A default TileLayer using OpenStreetMap data.
/// A default MapTileLayer using OpenStreetMap data.
/// </summary>
public static MapTileLayer OpenStreetMapTileLayer
{
@ -56,7 +55,7 @@ namespace MapControl
public static readonly DependencyProperty TileSourceProperty = DependencyProperty.Register(
nameof(TileSource), typeof(TileSource), typeof(MapTileLayer),
new PropertyMetadata(null, (o, e) => ((MapTileLayer)o).UpdateTiles(true)));
new PropertyMetadata(null, (o, e) => ((MapTileLayer)o).ResetTiles()));
public static readonly DependencyProperty SourceNameProperty = DependencyProperty.Register(
nameof(SourceName), typeof(string), typeof(MapTileLayer), new PropertyMetadata(null));
@ -84,9 +83,6 @@ namespace MapControl
public static readonly DependencyProperty UpdateWhileViewportChangingProperty = DependencyProperty.Register(
nameof(UpdateWhileViewportChanging), typeof(bool), typeof(MapTileLayer), new PropertyMetadata(true));
public static readonly DependencyProperty LoadTilesDescendingProperty = DependencyProperty.Register(
nameof(LoadTilesDescending), typeof(bool), typeof(MapTileLayer), new PropertyMetadata(false));
public static readonly DependencyProperty MapBackgroundProperty = DependencyProperty.Register(
nameof(MapBackground), typeof(Brush), typeof(MapTileLayer), new PropertyMetadata(null));
@ -138,7 +134,7 @@ namespace MapControl
}
/// <summary>
/// Description of the TileLayer.
/// Description of the MapTileLayer.
/// Used to display copyright information on top of the map.
/// </summary>
public string Description
@ -148,7 +144,7 @@ namespace MapControl
}
/// <summary>
/// Adds an offset to the Map's ZoomLevel for a relative scale between the Map and the TileLayer.
/// Adds an offset to the Map's ZoomLevel for a relative scale between the Map and the MapTileLayer.
/// </summary>
public double ZoomLevelOffset
{
@ -157,7 +153,7 @@ namespace MapControl
}
/// <summary>
/// Minimum zoom level supported by the TileLayer.
/// Minimum zoom level supported by the MapTileLayer.
/// </summary>
public int MinZoomLevel
{
@ -166,7 +162,7 @@ namespace MapControl
}
/// <summary>
/// Maximum zoom level supported by the TileLayer.
/// Maximum zoom level supported by the MapTileLayer.
/// </summary>
public int MaxZoomLevel
{
@ -175,7 +171,7 @@ namespace MapControl
}
/// <summary>
/// Maximum number of parallel downloads that may be performed by the TileLayer's ITileImageLoader.
/// Maximum number of parallel downloads that may be performed by the MapTileLayer's ITileImageLoader.
/// </summary>
public int MaxParallelDownloads
{
@ -201,19 +197,9 @@ namespace MapControl
set { SetValue(UpdateWhileViewportChangingProperty, value); }
}
/// <summary>
/// Controls the order of zoom levels in which map tiles are loaded.
/// The default is value is false, i.e. tiles are loaded in ascending order.
/// </summary>
public bool LoadTilesDescending
{
get { return (bool)GetValue(LoadTilesDescendingProperty); }
set { SetValue(LoadTilesDescendingProperty, value); }
}
/// <summary>
/// Optional background brush.
/// Sets MapBase.Background if not null and the TileLayer is the base map layer.
/// Sets MapBase.Background if not null and the MapTileLayer is the base map layer.
/// </summary>
public Brush MapBackground
{
@ -223,7 +209,7 @@ namespace MapControl
/// <summary>
/// Optional foreground brush.
/// Sets MapBase.Foreground if not null and the TileLayer is the base map layer.
/// Sets MapBase.Foreground if not null and the MapTileLayer is the base map layer.
/// </summary>
public Brush MapForeground
{
@ -295,13 +281,13 @@ namespace MapControl
{
TileGrid = tileGrid;
SetRenderTransform();
UpdateTiles(false);
UpdateTiles();
}
}
else
{
TileGrid = null;
UpdateTiles(true);
ResetTiles();
}
}
@ -368,38 +354,24 @@ namespace MapControl
MatrixEx.TranslateScaleRotateTranslate(tileOrigin, scale, parentMap.Heading, viewCenter);
}
private void UpdateTiles(bool clearTiles)
private void ResetTiles()
{
if (Tiles.Count > 0)
{
TileImageLoader.CancelLoadTiles(this);
}
if (clearTiles)
{
Tiles.Clear();
}
Tiles.Clear();
UpdateTiles();
}
private void UpdateTiles()
{
SelectTiles();
Children.Clear();
if (Tiles.Count > 0)
foreach (var tile in Tiles)
{
foreach (var tile in Tiles)
{
Children.Add(tile.Image);
}
var pendingTiles = Tiles.Where(t => t.Pending);
if (LoadTilesDescending)
{
pendingTiles = pendingTiles.OrderByDescending(t => t.ZoomLevel); // higher zoom levels first
}
TileImageLoader.BeginLoadTiles(this, pendingTiles);
Children.Add(tile.Image);
}
TileImageLoader.LoadTiles(this);
}
private void SelectTiles()
@ -409,13 +381,7 @@ namespace MapControl
if (parentMap != null && TileGrid != null && TileSource != null)
{
var maxZoomLevel = Math.Min(TileGrid.ZoomLevel, MaxZoomLevel);
var minZoomLevel = MinZoomLevel;
if (minZoomLevel < maxZoomLevel && this != parentMap.Children.Cast<UIElement>().FirstOrDefault())
{
// do not load background tiles if this is not the base layer
minZoomLevel = maxZoomLevel;
}
var minZoomLevel = parentMap.MapLayer == this ? MinZoomLevel : maxZoomLevel; // load background tiles only if this is the base layer
for (var z = minZoomLevel; z <= maxZoomLevel; z++)
{

View file

@ -23,7 +23,7 @@ namespace MapControl
}
/// <summary>
/// Used in TileLayer.
/// Used in MapTileLayer.
/// </summary>
public static Matrix TranslateScaleRotateTranslate(
Point translation1, double scale, double rotationAngle, Point translation2)

View file

@ -14,8 +14,8 @@ using System.Windows;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("© 2017 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("3.1.0")]
[assembly: AssemblyFileVersion("3.1.0")]
[assembly: AssemblyVersion("3.2.0")]
[assembly: AssemblyFileVersion("3.2.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

View file

@ -25,30 +25,27 @@ namespace MapControl
{
Pending = false;
if (image != null)
if (fadeIn && FadeDuration > TimeSpan.Zero)
{
if (fadeIn && FadeDuration > TimeSpan.Zero)
{
BitmapImage bitmap;
BitmapImage bitmap;
if (isDownloading && (bitmap = image as BitmapImage) != null)
{
bitmap.ImageOpened += BitmapImageOpened;
bitmap.ImageFailed += BitmapImageFailed;
}
else
{
Image.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation { From = 0d, To = 1d, Duration = FadeDuration });
}
if (isDownloading && (bitmap = image as BitmapImage) != null)
{
bitmap.ImageOpened += BitmapImageOpened;
bitmap.ImageFailed += BitmapImageFailed;
}
else
{
Image.Opacity = 1d;
Image.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation { From = 0d, To = 1d, Duration = FadeDuration });
}
Image.Source = image;
}
else
{
Image.Opacity = 1d;
}
Image.Source = image;
}
private void BitmapImageOpened(object sender, RoutedEventArgs e)

View file

@ -17,30 +17,39 @@ namespace MapControl
{
Pending = false;
if (image != null)
if (Image.Dispatcher.CheckAccess())
{
if (fadeIn && FadeDuration > TimeSpan.Zero)
{
var bitmap = image as BitmapSource;
SetImageSource(image, fadeIn);
}
else
{
Image.Dispatcher.BeginInvoke(new Action(() => SetImageSource(image, fadeIn)));
}
}
if (bitmap != null && !bitmap.IsFrozen && bitmap.IsDownloading)
{
bitmap.DownloadCompleted += BitmapDownloadCompleted;
bitmap.DownloadFailed += BitmapDownloadFailed;
}
else
{
Image.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(0d, 1d, FadeDuration));
}
private void SetImageSource(ImageSource image, bool fadeIn)
{
if (fadeIn && FadeDuration > TimeSpan.Zero)
{
var bitmap = image as BitmapSource;
if (bitmap != null && !bitmap.IsFrozen && bitmap.IsDownloading)
{
bitmap.DownloadCompleted += BitmapDownloadCompleted;
bitmap.DownloadFailed += BitmapDownloadFailed;
}
else
{
Image.Opacity = 1d;
Image.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(0d, 1d, FadeDuration));
}
Image.Source = image;
}
else
{
Image.Opacity = 1d;
}
Image.Source = image;
}
private void BitmapDownloadCompleted(object sender, EventArgs e)

View file

@ -13,7 +13,7 @@ namespace MapControl
{
public partial class Tile
{
public static TimeSpan FadeDuration { get; set; } = TimeSpan.FromSeconds(0.2);
public static TimeSpan FadeDuration { get; set; } = TimeSpan.FromSeconds(0.1);
public readonly int ZoomLevel;
public readonly int X;
@ -25,10 +25,9 @@ namespace MapControl
ZoomLevel = zoomLevel;
X = x;
Y = y;
Pending = true;
}
public bool Pending { get; private set; }
public bool Pending { get; set; } = true;
public int XIndex
{

View file

@ -3,7 +3,7 @@
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Windows.Media;
using System.Windows.Media.Imaging;
@ -15,41 +15,39 @@ namespace MapControl
/// </summary>
internal class TileImageLoader : ITileImageLoader
{
public void BeginLoadTiles(MapTileLayer tileLayer, IEnumerable<Tile> tiles)
public void LoadTiles(MapTileLayer tileLayer)
{
var imageTileSource = tileLayer.TileSource as ImageTileSource;
var tileSource = tileLayer.TileSource;
var imageTileSource = tileSource as ImageTileSource;
foreach (var tile in tiles)
foreach (var tile in tileLayer.Tiles.Where(t => t.Pending))
{
tile.Pending = false;
try
{
ImageSource image = null;
Uri uri;
if (imageTileSource != null)
{
image = imageTileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel);
}
else
else if ((uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel)) != null)
{
var uri = tileLayer.TileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
if (uri != null)
{
image = new BitmapImage(uri);
}
image = new BitmapImage(uri);
}
tile.SetImage(image);
if (image != null)
{
tile.SetImage(image);
}
}
catch (Exception ex)
{
Debug.WriteLine("Loading tile image failed: {0}", ex.Message);
Debug.WriteLine("{0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message);
}
}
}
public void CancelLoadTiles(MapTileLayer tileLayer)
{
}
}
}

View file

@ -14,7 +14,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Threading;
namespace MapControl
{
@ -63,195 +62,99 @@ namespace MapControl
Cache = MemoryCache.Default;
}
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 readonly ConcurrentStack<Tile> pendingTiles = new ConcurrentStack<Tile>();
private int taskCount;
public void BeginLoadTiles(MapTileLayer tileLayer, IEnumerable<Tile> tiles)
public void LoadTiles(MapTileLayer tileLayer)
{
if (tiles.Any())
pendingTiles.Clear();
var tileStack = tileLayer.Tiles.Where(t => t.Pending).Reverse().ToArray();
if (tileStack.Length > 0)
{
// get current TileLayer property values in UI thread
var dispatcher = tileLayer.Dispatcher;
pendingTiles.PushRange(tileStack);
var tileSource = tileLayer.TileSource;
var imageTileSource = tileSource as ImageTileSource;
var sourceName = tileLayer.SourceName;
var maxDownloads = tileLayer.MaxParallelDownloads;
if (imageTileSource != null && !imageTileSource.IsAsync) // call LoadImage in UI thread with low priority
while (taskCount < Math.Min(pendingTiles.Count, maxDownloads))
{
foreach (var tile in tiles)
Interlocked.Increment(ref taskCount);
Task.Run(() =>
{
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;
LoadPendingTiles(tileSource, sourceName);
Task.Run(() => GetTiles(tileList, dispatcher, tileSource, sourceName, maxDownloads));
Interlocked.Decrement(ref taskCount);
});
}
}
}
public void CancelLoadTiles(MapTileLayer 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.IsNullOrEmpty(sourceName)
&& !(tileSource is ImageTileSource)
&& !tileSource.UriFormat.StartsWith("file:");
foreach (var tile in tiles)
{
ImageSource 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)
private void LoadPendingTiles(TileSource tileSource, string sourceName)
{
var imageTileSource = tileSource as ImageTileSource;
PendingTile pendingTile;
Tile tile;
while (pendingTiles.TryDequeue(out pendingTile))
while (pendingTiles.TryPop(out tile))
{
var tile = pendingTile.Tile;
ImageSource image = null;
tile.Pending = false;
if (imageTileSource != null)
{
image = LoadImage(imageTileSource, tile);
}
else
{
var uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
if (uri != null)
{
if (!uri.IsAbsoluteUri)
{
image = LoadImage(uri.OriginalString);
}
else if (uri.Scheme == "file")
{
image = LoadImage(uri.LocalPath);
}
else
{
image = DownloadImage(uri, CacheKey(sourceName, tile))
?? pendingTile.CachedImage; // use possibly cached image if download failed
}
}
}
dispatcher.BeginInvoke(new Action<Tile, ImageSource>((t, i) => t.SetImage(i)), tile, image);
}
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(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))
ImageSource image = null;
Uri uri;
if (imageTileSource != null)
{
image = BitmapSourceHelper.FromStream(fileStream);
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);
}
}
catch (Exception ex)
{
Debug.WriteLine("{0}: {1}", path, ex.Message);
Debug.WriteLine("{0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message);
}
}
return image;
}
private static ImageSource DownloadImage(Uri uri, string cacheKey)
private ImageSource LoadImage(Uri uri, string sourceName, int x, int y, int zoomLevel)
{
ImageSource image = null;
try
{
var request = WebRequest.CreateHttp(uri);
if (HttpUserAgent != null)
if (!uri.IsAbsoluteUri)
{
request.UserAgent = HttpUserAgent;
image = BitmapSourceHelper.FromFile(uri.OriginalString);
}
using (var response = (HttpWebResponse)request.GetResponse())
else if (uri.Scheme == "file")
{
if (response.Headers["X-VE-Tile-Info"] != "no-tile") // set by Bing Maps
{
using (var responseStream = response.GetResponseStream())
using (var memoryStream = new MemoryStream())
{
responseStream.CopyTo(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
image = BitmapSourceHelper.FromStream(memoryStream);
image = BitmapSourceHelper.FromFile(uri.LocalPath);
}
else if (Cache == null || string.IsNullOrEmpty(sourceName))
{
image = DownloadImage(uri, null);
}
else
{
var cacheKey = string.Format("{0}/{1}/{2}/{3}", sourceName, zoomLevel, x, y);
if (cacheKey != null)
{
SetCachedImage(cacheKey, memoryStream, GetExpiration(response.Headers));
}
}
if (!GetCachedImage(cacheKey, ref image))
{
// 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);
}
}
}
@ -267,15 +170,41 @@ namespace MapControl
return image;
}
private static string CacheKey(string sourceName, Tile tile)
private static ImageSource DownloadImage(Uri uri, string cacheKey)
{
return string.IsNullOrEmpty(sourceName) ? null : string.Format("{0}/{1}/{2}/{3}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y);
ImageSource image = null;
var request = WebRequest.CreateHttp(uri);
if (HttpUserAgent != null)
{
request.UserAgent = HttpUserAgent;
}
using (var response = (HttpWebResponse)request.GetResponse())
{
if (response.Headers["X-VE-Tile-Info"] != "no-tile") // set by Bing Maps
{
using (var responseStream = response.GetResponseStream())
using (var memoryStream = new MemoryStream())
{
responseStream.CopyTo(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
image = BitmapSourceHelper.FromStream(memoryStream);
if (cacheKey != null)
{
SetCachedImage(cacheKey, memoryStream, GetExpiration(response.Headers));
}
}
}
}
return image;
}
private static bool GetCachedImage(string cacheKey, out ImageSource image)
private static bool GetCachedImage(string cacheKey, ref ImageSource image)
{
image = null;
var result = false;
var buffer = Cache.Get(cacheKey) as byte[];
if (buffer != null)
@ -294,7 +223,7 @@ namespace MapControl
expiration = new DateTime(BitConverter.ToInt64(buffer, buffer.Length - 8), DateTimeKind.Utc);
}
return expiration > DateTime.UtcNow;
result = expiration > DateTime.UtcNow;
}
catch (Exception ex)
{
@ -302,7 +231,7 @@ namespace MapControl
}
}
return false;
return result;
}
private static void SetCachedImage(string cacheKey, MemoryStream memoryStream, DateTime expiration)

View file

@ -7,6 +7,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.Storage;
@ -56,130 +57,124 @@ namespace MapControl
MinimumCacheExpiration = TimeSpan.FromHours(1);
}
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 readonly ConcurrentStack<Tile> pendingTiles = new ConcurrentStack<Tile>();
private int taskCount;
public void BeginLoadTiles(MapTileLayer tileLayer, IEnumerable<Tile> tiles)
public void LoadTiles(MapTileLayer tileLayer)
{
pendingTiles.Clear();
var tileSource = tileLayer.TileSource;
var imageTileSource = tileSource as ImageTileSource;
var tiles = tileLayer.Tiles.Where(t => t.Pending);
if (imageTileSource != null)
{
foreach (var tile in tiles)
{
try
{
tile.SetImage(imageTileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel));
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
LoadTiles(imageTileSource, tiles);
}
else
{
foreach (var tile in tiles)
var tileStack = tiles.Reverse().ToArray();
if (tileStack.Length > 0)
{
Uri uri = null;
pendingTiles.PushRange(tileStack);
try
{
uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
var sourceName = tileLayer.SourceName;
var maxDownloads = tileLayer.MaxParallelDownloads;
if (uri == null)
while (taskCount < Math.Min(pendingTiles.Count, maxDownloads))
{
tile.SetImage(null);
}
else
{
pendingTiles.Enqueue(new PendingTile(tile, uri));
Interlocked.Increment(ref taskCount);
var newTaskCount = Math.Min(pendingTiles.Count, tileLayer.MaxParallelDownloads) - taskCount;
var sourceName = tileLayer.SourceName;
while (newTaskCount-- > 0)
Task.Run(async () =>
{
Interlocked.Increment(ref taskCount);
await LoadPendingTiles(tileSource, sourceName);
Task.Run(() => LoadPendingTiles(tileSource, sourceName)); // Task.Run(Func<Task>)
}
Interlocked.Decrement(ref taskCount);
});
}
}
}
}
public void CancelLoadTiles(MapTileLayer tileLayer)
private void LoadTiles(ImageTileSource tileSource, IEnumerable<Tile> tiles)
{
PendingTile pendingTile;
foreach (var tile in tiles)
{
tile.Pending = false;
while (pendingTiles.TryDequeue(out pendingTile)) ; // no Clear method
try
{
var image = tileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel);
if (image != null)
{
tile.SetImage(image);
}
}
catch (Exception ex)
{
Debug.WriteLine("{0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message);
}
}
}
private async Task LoadPendingTiles(TileSource tileSource, string sourceName)
{
PendingTile pendingTile;
Tile tile;
while (pendingTiles.TryDequeue(out pendingTile))
while (pendingTiles.TryPop(out tile))
{
var tile = pendingTile.Tile;
var uri = pendingTile.Uri;
tile.Pending = false;
if (Cache == null || sourceName == null)
try
{
await DownloadImage(tile, uri, null);
}
else
{
var extension = Path.GetExtension(uri.LocalPath);
var uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
if (string.IsNullOrEmpty(extension) || extension == ".jpeg")
if (uri != null)
{
extension = ".jpg";
}
var cacheKey = string.Format(@"{0}\{1}\{2}\{3}{4}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y, extension);
var cacheItem = await Cache.GetAsync(cacheKey);
var loaded = false;
if (cacheItem == null || cacheItem.Expiration <= DateTime.UtcNow)
{
loaded = await DownloadImage(tile, uri, cacheKey);
}
if (!loaded && cacheItem != null && cacheItem.Buffer != null)
{
using (var stream = new InMemoryRandomAccessStream())
if (Cache == null || sourceName == null)
{
await stream.WriteAsync(cacheItem.Buffer);
await stream.FlushAsync();
stream.Seek(0);
await DownloadImage(tile, uri, null);
}
else
{
var extension = Path.GetExtension(uri.LocalPath);
await LoadImageFromStream(tile, stream);
if (string.IsNullOrEmpty(extension) || extension == ".jpeg")
{
extension = ".jpg";
}
var cacheKey = string.Format(@"{0}\{1}\{2}\{3}{4}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y, extension);
var cacheItem = await Cache.GetAsync(cacheKey);
var loaded = false;
if (cacheItem == null || cacheItem.Expiration <= DateTime.UtcNow)
{
loaded = await DownloadImage(tile, uri, cacheKey);
}
if (!loaded && cacheItem != null && cacheItem.Buffer != null)
{
using (var stream = new InMemoryRandomAccessStream())
{
await stream.WriteAsync(cacheItem.Buffer);
await stream.FlushAsync();
stream.Seek(0);
await LoadImageFromStream(tile, stream);
}
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine("{0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message);
}
}
Interlocked.Decrement(ref taskCount);
}
private async Task<bool> DownloadImage(Tile tile, Uri uri, string cacheKey)
@ -211,7 +206,6 @@ namespace MapControl
if (response.Headers.TryGetValue("X-VE-Tile-Info", out tileInfo) && tileInfo == "no-tile") // set by Bing Maps
{
tile.SetImage(null);
return true;
}
@ -255,28 +249,25 @@ namespace MapControl
private async Task<bool> LoadImageFromStream(Tile tile, IRandomAccessStream stream)
{
var completion = new TaskCompletionSource<bool>();
var tcs = new TaskCompletionSource<bool>();
var action = tile.Image.Dispatcher.RunAsync(
CoreDispatcherPriority.Normal,
async () =>
await tile.Image.Dispatcher.RunAsync(CoreDispatcherPriority.Low, async () =>
{
try
{
try
{
var image = new BitmapImage();
await image.SetSourceAsync(stream);
tile.SetImage(image, true, false);
completion.SetResult(true);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
tile.SetImage(null);
completion.SetResult(false);
}
});
var image = new BitmapImage();
await image.SetSourceAsync(stream);
tile.SetImage(image, true, false);
tcs.SetResult(true);
}
catch (Exception ex)
{
Debug.WriteLine("{0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message);
tcs.SetResult(false);
}
});
return await completion.Task;
return await tcs.Task;
}
}
}

View file

@ -15,7 +15,7 @@ namespace MapControl
}
/// <summary>
/// Indicates if the map projection has changed, i.e. if a TileLayer or ImageLayer should be
/// Indicates if the map projection has changed, i.e. if a MapTileLayer or MapImageLayer should be
/// immediately updated, or MapPath Data in cartesian map coordinates should be recalculated.
/// </summary>
public bool ProjectionChanged { get; }

View file

@ -7,8 +7,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("© 2017 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("3.1.0")]
[assembly: AssemblyFileVersion("3.1.0")]
[assembly: AssemblyVersion("3.2.0")]
[assembly: AssemblyFileVersion("3.2.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

View file

@ -29,7 +29,8 @@ namespace ViewModel
{
SourceName = "OpenStreetMap German",
Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)",
TileSource = new TileSource { UriFormat = "http://{c}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png" }
TileSource = new TileSource { UriFormat = "http://{c}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png" },
MaxZoomLevel = 19
}
},
{

View file

@ -7,8 +7,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("© 2017 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("3.1.0")]
[assembly: AssemblyFileVersion("3.1.0")]
[assembly: AssemblyVersion("3.2.0")]
[assembly: AssemblyFileVersion("3.2.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

View file

@ -7,8 +7,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("© 2017 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("3.1.0")]
[assembly: AssemblyFileVersion("3.1.0")]
[assembly: AssemblyVersion("3.2.0")]
[assembly: AssemblyFileVersion("3.2.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

View file

@ -11,8 +11,8 @@ namespace UniversalApp
public MainPage()
{
//TileImageLoader.Cache = new MapControl.Caching.ImageFileCache();
//TileImageLoader.Cache = new MapControl.Caching.FileDbCache();
//MapControl.TileImageLoader.Cache = new MapControl.Caching.ImageFileCache();
//MapControl.TileImageLoader.Cache = new MapControl.Caching.FileDbCache();
InitializeComponent();
DataContext = ViewModel;

View file

@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("© 2017 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyVersion("3.1.0")]
[assembly: AssemblyFileVersion("3.1.0")]
[assembly: AssemblyVersion("3.2.0")]
[assembly: AssemblyFileVersion("3.2.0")]
[assembly: AssemblyConfiguration("")]
[assembly: ComVisible(false)]

View file

@ -7,8 +7,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("© 2017 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("3.1.0")]
[assembly: AssemblyFileVersion("3.1.0")]
[assembly: AssemblyVersion("3.2.0")]
[assembly: AssemblyFileVersion("3.2.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]