Version 7.3. Added tile and map image progress reporting.

This commit is contained in:
Clemens 2022-08-05 18:54:19 +02:00
parent b423cc2d36
commit 3119c0fc9b
32 changed files with 252 additions and 100 deletions

View file

@ -29,7 +29,7 @@ namespace MapControl
public static HttpClient HttpClient { get; set; } = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
public static async Task<ImageSource> LoadImageAsync(Uri uri)
public static async Task<ImageSource> LoadImageAsync(Uri uri, IProgress<double> progress = null)
{
ImageSource image = null;
@ -41,7 +41,7 @@ namespace MapControl
}
else if (uri.Scheme == "http" || uri.Scheme == "https")
{
var response = await GetHttpResponseAsync(uri);
var response = await GetHttpResponseAsync(uri, progress);
if (response != null && response.Buffer != null)
{
@ -73,10 +73,12 @@ namespace MapControl
}
}
internal static async Task<HttpResponse> GetHttpResponseAsync(Uri uri)
internal static async Task<HttpResponse> GetHttpResponseAsync(Uri uri, IProgress<double> progress = null)
{
HttpResponse response = null;
progress?.Report(0d);
try
{
using (var responseMessage = await HttpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
@ -85,10 +87,12 @@ namespace MapControl
{
byte[] buffer = null;
// check for possibly unavailable Bing Maps tile
//
if (!responseMessage.Headers.TryGetValues("X-VE-Tile-Info", out IEnumerable<string> tileInfo) ||
!tileInfo.Contains("no-tile"))
{
buffer = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
buffer = await ReadAsByteArrayAsync(responseMessage.Content, progress).ConfigureAwait(false);
}
response = new HttpResponse(buffer, responseMessage.Headers.CacheControl?.MaxAge);
@ -104,7 +108,39 @@ namespace MapControl
Debug.WriteLine($"ImageLoader: {uri}: {ex.Message}");
}
progress?.Report(1d);
return response;
}
private static async Task<byte[]> ReadAsByteArrayAsync(HttpContent content, IProgress<double> progress)
{
if (progress == null || !content.Headers.ContentLength.HasValue)
{
return await content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
var length = (int)content.Headers.ContentLength.Value;
var buffer = new byte[length];
using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false))
{
int offset = 0;
int read;
while (offset < length &&
(read = await stream.ReadAsync(buffer, offset, length - offset).ConfigureAwait(false)) > 0)
{
offset += read;
if (offset < length) // 1.0 reported by caller
{
progress.Report((double)offset / length);
}
}
}
return buffer;
}
}
}

View file

@ -54,11 +54,17 @@ namespace MapControl
public static readonly DependencyProperty MapForegroundProperty = DependencyProperty.Register(
nameof(MapForeground), typeof(Brush), typeof(MapImageLayer), new PropertyMetadata(null));
public static readonly DependencyProperty LoadingProgressProperty = DependencyProperty.Register(
nameof(LoadingProgress), typeof(double), typeof(MapImageLayer), new PropertyMetadata(1d));
private readonly Progress<double> imageProgress;
private readonly DispatcherTimer updateTimer;
private bool updateInProgress;
public MapImageLayer()
{
imageProgress = new Progress<double>(p => LoadingProgress = p);
updateTimer = this.CreateTimer(UpdateInterval);
updateTimer.Tick += async (s, e) => await UpdateImageAsync();
}
@ -119,13 +125,20 @@ namespace MapControl
set { SetValue(MapForegroundProperty, value); }
}
/// <summary>
/// Gets the progress of the ImageLoader as a double value between 0 and 1.
/// </summary>
public double LoadingProgress
{
get { return (double)GetValue(LoadingProgressProperty); }
private set { SetValue(LoadingProgressProperty, value); }
}
/// <summary>
/// The current BoundingBox
/// </summary>
public BoundingBox BoundingBox { get; private set; }
protected abstract Task<ImageSource> GetImageAsync();
protected override void SetParentMap(MapBase map)
{
if (map == null)
@ -165,38 +178,43 @@ namespace MapControl
protected async Task UpdateImageAsync()
{
updateTimer.Stop();
if (updateInProgress)
if (updateInProgress) // update image on next tick
{
updateTimer.Run(); // update image on next timer tick
updateTimer.Run(); // start timer if not running
}
else if (ParentMap != null && ParentMap.RenderSize.Width > 0 && ParentMap.RenderSize.Height > 0)
else
{
updateInProgress = true;
updateTimer.Stop();
UpdateBoundingBox();
ImageSource image = null;
if (BoundingBox != null)
if (ParentMap != null && ParentMap.RenderSize.Width > 0 && ParentMap.RenderSize.Height > 0)
{
try
updateInProgress = true;
UpdateBoundingBox();
ImageSource image = null;
if (BoundingBox != null)
{
image = await GetImageAsync();
}
catch (Exception ex)
{
Debug.WriteLine($"MapImageLayer: {ex.Message}");
try
{
image = await GetImageAsync(imageProgress);
}
catch (Exception ex)
{
Debug.WriteLine($"MapImageLayer: {ex.Message}");
}
}
SwapImages(image);
updateInProgress = false;
}
SwapImages(image);
updateInProgress = false;
}
}
protected abstract Task<ImageSource> GetImageAsync(IProgress<double> progress);
private void UpdateBoundingBox()
{
var width = ParentMap.RenderSize.Width * RelativeImageSize;

View file

@ -25,6 +25,8 @@ namespace MapControl
{
public interface ITileImageLoader
{
IProgress<double> Progress { get; set; }
TileSource TileSource { get; }
Task LoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName);
@ -58,13 +60,19 @@ namespace MapControl
public static readonly DependencyProperty MapForegroundProperty = DependencyProperty.Register(
nameof(MapForeground), typeof(Brush), typeof(MapTileLayerBase), new PropertyMetadata(null));
public static readonly DependencyProperty LoadingProgressProperty = DependencyProperty.Register(
nameof(LoadingProgress), typeof(double), typeof(MapTileLayerBase), new PropertyMetadata(1d,
(o, e) => { System.Diagnostics.Debug.WriteLine("LoadingProgress = {0:P0}", e.NewValue); }));
private readonly DispatcherTimer updateTimer;
private MapBase parentMap;
protected MapTileLayerBase(ITileImageLoader tileImageLoader)
{
RenderTransform = new MatrixTransform();
TileImageLoader = tileImageLoader;
TileImageLoader.Progress = new Progress<double>(p => LoadingProgress = p);
updateTimer = this.CreateTimer(UpdateInterval);
updateTimer.Tick += async (s, e) => await Update();
@ -149,6 +157,15 @@ namespace MapControl
set { SetValue(MapForegroundProperty, value); }
}
/// <summary>
/// Gets the progress of the TileImageLoader as a double value between 0 and 1.
/// </summary>
public double LoadingProgress
{
get { return (double)GetValue(LoadingProgressProperty); }
private set { SetValue(LoadingProgressProperty, value); }
}
public MapBase ParentMap
{
get { return parentMap; }

View file

@ -9,6 +9,7 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MapControl
@ -18,6 +19,35 @@ namespace MapControl
/// </summary>
public partial class TileImageLoader : ITileImageLoader
{
private class TileQueue : ConcurrentStack<Tile>
{
public TileQueue(IEnumerable<Tile> tiles)
: base(tiles.Where(tile => tile.Pending).Reverse())
{
}
public bool IsCanceled { get; private set; }
public bool TryDequeue(out Tile tile)
{
tile = null;
if (IsCanceled || !TryPop(out tile))
{
return false;
}
tile.Pending = false;
return true;
}
public void Cancel()
{
IsCanceled = true;
Clear();
}
}
/// <summary>
/// Maximum number of parallel tile loading tasks. The default value is 4.
/// </summary>
@ -35,12 +65,19 @@ namespace MapControl
/// </summary>
public static TimeSpan MaxCacheExpiration { get; set; } = TimeSpan.FromDays(10);
/// <summary>
/// Reports tile loading process as double value between 0 and 1.
/// </summary>
public IProgress<double> Progress { get; set; }
/// <summary>
/// The current TileSource, passed to the most recent LoadTiles call.
/// </summary>
public TileSource TileSource { get; private set; }
private ConcurrentStack<Tile> pendingTiles;
private TileQueue pendingTiles;
private int progressTotal;
private int progressLoaded;
/// <summary>
/// Loads all pending tiles from the tiles collection.
@ -49,39 +86,47 @@ namespace MapControl
/// </summary>
public Task LoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName)
{
pendingTiles?.Clear(); // stop processing the current queue
pendingTiles?.Cancel();
TileSource = tileSource;
if (tileSource != null)
{
pendingTiles = new ConcurrentStack<Tile>(tiles.Where(tile => tile.Pending).Reverse());
pendingTiles = new TileQueue(tiles);
var numTasks = Math.Min(pendingTiles.Count, MaxLoadTasks);
if (numTasks > 0)
{
if (Progress != null)
{
progressTotal = pendingTiles.Count;
progressLoaded = 0;
Progress.Report(0d);
}
if (Cache == null || tileSource.UriFormat == null || !tileSource.UriFormat.StartsWith("http"))
{
cacheName = null; // no tile caching
}
var tasks = Enumerable.Range(0, numTasks)
.Select(_ => Task.Run(() => LoadPendingTiles(pendingTiles, tileSource, cacheName)));
return Task.WhenAll(tasks);
return Task.WhenAll(Enumerable.Range(0, numTasks).Select(
_ => Task.Run(() => LoadPendingTiles(pendingTiles, tileSource, cacheName))));
}
}
if (Progress != null && progressLoaded < progressTotal)
{
Progress.Report(1d);
}
return Task.CompletedTask;
}
private static async Task LoadPendingTiles(ConcurrentStack<Tile> pendingTiles, TileSource tileSource, string cacheName)
private async Task LoadPendingTiles(TileQueue tileQueue, TileSource tileSource, string cacheName)
{
while (pendingTiles.TryPop(out var tile))
while (tileQueue.TryDequeue(out var tile))
{
tile.Pending = false;
try
{
await LoadTile(tile, tileSource, cacheName).ConfigureAwait(false);
@ -90,6 +135,13 @@ namespace MapControl
{
Debug.WriteLine($"TileImageLoader: {tile.ZoomLevel}/{tile.XIndex}/{tile.Y}: {ex.Message}");
}
if (Progress != null && !tileQueue.IsCanceled)
{
Interlocked.Increment(ref progressLoaded);
Progress.Report((double)progressLoaded / progressTotal);
}
}
}

View file

@ -34,7 +34,14 @@ namespace MapControl
public static readonly DependencyProperty LayersProperty = DependencyProperty.Register(
nameof(Layers), typeof(string), typeof(WmsImageLayer),
new PropertyMetadata(null, async (o, e) => await ((WmsImageLayer)o).UpdateImageAsync()));
new PropertyMetadata(null,
async (o, e) =>
{
if (e.OldValue != null) // ignore initial property change from GetImageAsync
{
await ((WmsImageLayer)o).UpdateImageAsync();
}
}));
public static readonly DependencyProperty StylesProperty = DependencyProperty.Register(
nameof(Styles), typeof(string), typeof(WmsImageLayer),
@ -187,7 +194,7 @@ namespace MapControl
/// <summary>
/// Loads an ImageSource from the URL returned by GetMapRequestUri().
/// </summary>
protected override async Task<ImageSource> GetImageAsync()
protected override async Task<ImageSource> GetImageAsync(IProgress<double> progress)
{
ImageSource image = null;
@ -203,7 +210,7 @@ namespace MapControl
if (!string.IsNullOrEmpty(uri))
{
image = await ImageLoader.LoadImageAsync(new Uri(uri));
image = await ImageLoader.LoadImageAsync(new Uri(uri), progress);
}
}