Improved TileImageLoader

This commit is contained in:
Clemens 2021-07-05 00:03:44 +02:00
parent fb00a11f4b
commit 5fde94acb9
7 changed files with 88 additions and 60 deletions

View file

@ -8,6 +8,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
#if WINUI #if WINUI
using Windows.Foundation; using Windows.Foundation;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
@ -68,7 +69,11 @@ namespace MapControl
public static readonly DependencyProperty MapForegroundProperty = DependencyProperty.Register( public static readonly DependencyProperty MapForegroundProperty = DependencyProperty.Register(
nameof(MapForeground), typeof(Brush), typeof(MapImageLayer), new PropertyMetadata(null)); nameof(MapForeground), typeof(Brush), typeof(MapImageLayer), new PropertyMetadata(null));
private readonly DispatcherTimer updateTimer; #if WINUI
private readonly DispatcherQueueTimer updateTimer;
#else
private readonly DispatcherTimer updateTimer = new DispatcherTimer();
#endif
private bool updateInProgress; private bool updateInProgress;
public MapImageLayer() public MapImageLayer()
@ -76,7 +81,10 @@ namespace MapControl
Children.Add(new Image { Opacity = 0d, Stretch = Stretch.Fill }); Children.Add(new Image { Opacity = 0d, Stretch = Stretch.Fill });
Children.Add(new Image { Opacity = 0d, Stretch = Stretch.Fill }); Children.Add(new Image { Opacity = 0d, Stretch = Stretch.Fill });
updateTimer = new DispatcherTimer { Interval = UpdateInterval }; #if WINUI
updateTimer = DispatcherQueue.CreateTimer();
#endif
updateTimer.Interval = UpdateInterval;
updateTimer.Tick += async (s, e) => await UpdateImageAsync(); updateTimer.Tick += async (s, e) => await UpdateImageAsync();
} }

View file

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
#if WINUI #if WINUI
using Windows.Foundation; using Windows.Foundation;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@ -115,7 +116,7 @@ namespace MapControl
return finalSize; return finalSize;
} }
protected override void UpdateTileLayer(bool tileSourceChanged) protected override Task UpdateTileLayer()
{ {
var update = false; var update = false;
@ -126,7 +127,7 @@ namespace MapControl
} }
else else
{ {
if (tileSourceChanged) if (TileSource != TileImageLoader.TileSource)
{ {
Tiles.Clear(); Tiles.Clear();
update = true; update = true;
@ -141,7 +142,7 @@ namespace MapControl
if (update) if (update)
{ {
SetTiles(); UpdateTiles();
Children.Clear(); Children.Clear();
@ -150,8 +151,10 @@ namespace MapControl
Children.Add(tile.Image); Children.Add(tile.Image);
} }
LoadTiles(Tiles, SourceName); return TileImageLoader.LoadTiles(Tiles, TileSource, SourceName);
} }
return Task.CompletedTask;
} }
protected override void SetRenderTransform() protected override void SetRenderTransform()
@ -196,7 +199,7 @@ namespace MapControl
return true; return true;
} }
private void SetTiles() private void UpdateTiles()
{ {
int maxZoomLevel; int maxZoomLevel;

View file

@ -4,7 +4,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
#if WINUI #if WINUI
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
@ -23,14 +25,16 @@ namespace MapControl
{ {
public interface ITileImageLoader public interface ITileImageLoader
{ {
void LoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName); TileSource TileSource { get; }
Task LoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName);
} }
public abstract class MapTileLayerBase : Panel, IMapLayer public abstract class MapTileLayerBase : Panel, IMapLayer
{ {
public static readonly DependencyProperty TileSourceProperty = DependencyProperty.Register( public static readonly DependencyProperty TileSourceProperty = DependencyProperty.Register(
nameof(TileSource), typeof(TileSource), typeof(MapTileLayerBase), nameof(TileSource), typeof(TileSource), typeof(MapTileLayerBase),
new PropertyMetadata(null, (o, e) => ((MapTileLayerBase)o).Update(true))); new PropertyMetadata(null, async (o, e) => await ((MapTileLayerBase)o).Update()));
public static readonly DependencyProperty SourceNameProperty = DependencyProperty.Register( public static readonly DependencyProperty SourceNameProperty = DependencyProperty.Register(
nameof(SourceName), typeof(string), typeof(MapTileLayerBase), new PropertyMetadata(null)); nameof(SourceName), typeof(string), typeof(MapTileLayerBase), new PropertyMetadata(null));
@ -54,7 +58,11 @@ namespace MapControl
public static readonly DependencyProperty MapForegroundProperty = DependencyProperty.Register( public static readonly DependencyProperty MapForegroundProperty = DependencyProperty.Register(
nameof(MapForeground), typeof(Brush), typeof(MapTileLayerBase), new PropertyMetadata(null)); nameof(MapForeground), typeof(Brush), typeof(MapTileLayerBase), new PropertyMetadata(null));
private readonly DispatcherTimer updateTimer; #if WINUI
private readonly DispatcherQueueTimer updateTimer;
#else
private readonly DispatcherTimer updateTimer = new DispatcherTimer();
#endif
private MapBase parentMap; private MapBase parentMap;
protected MapTileLayerBase(ITileImageLoader tileImageLoader) protected MapTileLayerBase(ITileImageLoader tileImageLoader)
@ -62,8 +70,11 @@ namespace MapControl
RenderTransform = new MatrixTransform(); RenderTransform = new MatrixTransform();
TileImageLoader = tileImageLoader; TileImageLoader = tileImageLoader;
updateTimer = new DispatcherTimer { Interval = UpdateInterval }; #if WINUI
updateTimer.Tick += (s, e) => Update(false); updateTimer = DispatcherQueue.CreateTimer();
#endif
updateTimer.Interval = UpdateInterval;
updateTimer.Tick += async (s, e) => await Update();
#if WINUI || WINDOWS_UWP #if WINUI || WINDOWS_UWP
MapPanel.InitMapElement(this); MapPanel.InitMapElement(this);
@ -162,15 +173,15 @@ namespace MapControl
parentMap.ViewportChanged += OnViewportChanged; parentMap.ViewportChanged += OnViewportChanged;
} }
Update(false); updateTimer.Start();
} }
} }
private void OnViewportChanged(object sender, ViewportChangedEventArgs e) private async void OnViewportChanged(object sender, ViewportChangedEventArgs e)
{ {
if (Children.Count == 0 || e.ProjectionChanged || Math.Abs(e.LongitudeOffset) > 180d) if (Children.Count == 0 || e.ProjectionChanged || Math.Abs(e.LongitudeOffset) > 180d)
{ {
Update(false); // update immediately when projection has changed or center has moved across 180° longitude await Update(); // update immediately when projection has changed or center has moved across 180° longitude
} }
else else
{ {
@ -185,20 +196,15 @@ namespace MapControl
} }
} }
private void Update(bool tileSourceChanged) private Task Update()
{ {
updateTimer.Stop(); updateTimer.Stop();
UpdateTileLayer(tileSourceChanged); return UpdateTileLayer();
} }
protected abstract void UpdateTileLayer(bool tileSourceChanged); protected abstract Task UpdateTileLayer();
protected abstract void SetRenderTransform(); protected abstract void SetRenderTransform();
protected virtual void LoadTiles(IEnumerable<Tile> tiles, string cacheName)
{
TileImageLoader.LoadTiles(tiles, TileSource, cacheName);
}
} }
} }

View file

@ -6,6 +6,7 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -45,33 +46,28 @@ namespace MapControl
/// </summary> /// </summary>
public static TimeSpan MaxCacheExpiration { get; set; } = TimeSpan.FromDays(10); public static TimeSpan MaxCacheExpiration { get; set; } = TimeSpan.FromDays(10);
public TileSource TileSource { get; private set; }
private readonly ConcurrentQueue<Tile> tileQueue = new ConcurrentQueue<Tile>(); private readonly ConcurrentQueue<Tile> tileQueue = new ConcurrentQueue<Tile>();
private TileSource tileSource;
private string cacheName;
private int taskCount; private int taskCount;
/// <summary> /// <summary>
/// Loads all pending tiles from the tiles collection. /// Loads all pending tiles from the tiles collection.
/// If source.UriFormat starts with "http" and cache is a non-empty string, /// If tileSource.UriFormat starts with "http" and tileCacheName is a non-empty string,
/// tile images will be cached in the TileImageLoader's Cache (if that is not null). /// tile images will be cached in the TileImageLoader's Cache (if that is not null).
/// </summary> /// </summary>
public void LoadTiles(IEnumerable<Tile> tiles, TileSource source, string cache) public async Task LoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName)
{ {
tileQueue.Clear(); tileQueue.Clear();
TileSource = tileSource;
tiles = tiles.Where(tile => tile.Pending); tiles = tiles.Where(tile => tile.Pending);
tileSource = source; if (tileSource != null && tiles.Any())
cacheName = null;
if (tiles.Any() && tileSource != null)
{ {
if (!string.IsNullOrEmpty(cache) && if (string.IsNullOrEmpty(cacheName) || Cache == null ||
Cache != null && tileSource.UriFormat == null || !tileSource.UriFormat.StartsWith("http"))
tileSource.UriFormat != null &&
tileSource.UriFormat.StartsWith("http"))
{ {
cacheName = cache; cacheName = null;
} }
foreach (var tile in tiles) foreach (var tile in tiles)
@ -79,28 +75,28 @@ namespace MapControl
tileQueue.Enqueue(tile); tileQueue.Enqueue(tile);
} }
var tasks = new List<Task>();
while (taskCount < Math.Min(tileQueue.Count, MaxLoadTasks)) while (taskCount < Math.Min(tileQueue.Count, MaxLoadTasks))
{ {
Interlocked.Increment(ref taskCount); Interlocked.Increment(ref taskCount);
Task.Run(LoadTilesFromQueueAsync); tasks.Add(LoadTilesFromQueueAsync(tileSource, cacheName));
} }
await Task.WhenAll(tasks);
} }
} }
private async Task LoadTilesFromQueueAsync() private async Task LoadTilesFromQueueAsync(TileSource tileSource, string cacheName)
{ {
// tileSource or cacheName may change after dequeuing a tile while (tileQueue.TryDequeue(out var tile))
var source = tileSource;
var cache = cacheName;
while (tileQueue.TryDequeue(out Tile tile))
{ {
tile.Pending = false; tile.Pending = false;
try try
{ {
await LoadTileAsync(tile, source, cache).ConfigureAwait(false); await LoadTileAsync(tile, tileSource, cacheName).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -132,7 +128,8 @@ namespace MapControl
extension = ".jpg"; extension = ".jpg";
} }
var cacheKey = string.Format("{0}/{1}/{2}/{3}{4}", cacheName, tile.ZoomLevel, tile.XIndex, tile.Y, extension); var cacheKey = string.Format(CultureInfo.InvariantCulture,
"{0}/{1}/{2}/{3}{4}", cacheName, tile.ZoomLevel, tile.XIndex, tile.Y, extension);
return LoadCachedTileAsync(tile, uri, cacheKey); return LoadCachedTileAsync(tile, uri, cacheKey);
} }

View file

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
#if WINUI #if WINUI
using Windows.Foundation; using Windows.Foundation;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@ -86,19 +87,24 @@ namespace MapControl
return finalSize; return finalSize;
} }
protected override void UpdateTileLayer(bool tileSourceChanged) protected override Task UpdateTileLayer()
{ {
if (ParentMap == null || if (ParentMap == null ||
!TileMatrixSets.TryGetValue(ParentMap.MapProjection.CrsId, out WmtsTileMatrixSet tileMatrixSet)) !TileMatrixSets.TryGetValue(ParentMap.MapProjection.CrsId, out WmtsTileMatrixSet tileMatrixSet))
{ {
Children.Clear(); Children.Clear();
UpdateTiles(null);
return UpdateTiles(null);
} }
else if (UpdateChildLayers(tileMatrixSet))
if (UpdateChildLayers(tileMatrixSet))
{ {
SetRenderTransform(); SetRenderTransform();
UpdateTiles(tileMatrixSet);
return UpdateTiles(tileMatrixSet);
} }
return Task.CompletedTask;
} }
protected override void SetRenderTransform() protected override void SetRenderTransform()
@ -154,7 +160,7 @@ namespace MapControl
return layersChanged; return layersChanged;
} }
private void UpdateTiles(WmtsTileMatrixSet tileMatrixSet) private Task UpdateTiles(WmtsTileMatrixSet tileMatrixSet)
{ {
var tiles = new List<Tile>(); var tiles = new List<Tile>();
var cacheName = SourceName; var cacheName = SourceName;
@ -180,7 +186,7 @@ namespace MapControl
} }
} }
LoadTiles(tiles, cacheName); return TileImageLoader.LoadTiles(tiles, TileSource, cacheName);
} }
private async void OnLoaded(object sender, RoutedEventArgs e) private async void OnLoaded(object sender, RoutedEventArgs e)

View file

@ -31,7 +31,7 @@ namespace MapControl
} }
/// <summary> /// <summary>
/// An IImageCache implementation used to cache tile images. The default is null. /// An IImageCache implementation used to cache tile images.
/// </summary> /// </summary>
public static Caching.IImageCache Cache { get; set; } public static Caching.IImageCache Cache { get; set; }
@ -69,7 +69,7 @@ namespace MapControl
{ {
var tcs = new TaskCompletionSource<object>(); var tcs = new TaskCompletionSource<object>();
await tile.Image.Dispatcher.RunAsync(CoreDispatcherPriority.Low, async () => async void callback()
{ {
try try
{ {
@ -80,7 +80,9 @@ namespace MapControl
{ {
tcs.TrySetException(ex); tcs.TrySetException(ex);
} }
}); }
await tile.Image.Dispatcher.RunAsync(CoreDispatcherPriority.Low, callback);
await tcs.Task.ConfigureAwait(false); await tcs.Task.ConfigureAwait(false);
} }

View file

@ -2,11 +2,11 @@
// © 2021 Clemens Fischer // © 2021 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL) // Licensed under the Microsoft Public License (Ms-PL)
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
namespace MapControl namespace MapControl
{ {
@ -31,7 +31,7 @@ namespace MapControl
} }
/// <summary> /// <summary>
/// An IImageCache implementation used to cache tile images. The default is null. /// An IImageCache implementation used to cache tile images.
/// </summary> /// </summary>
public static Caching.IImageCache Cache { get; set; } public static Caching.IImageCache Cache { get; set; }
@ -69,7 +69,7 @@ namespace MapControl
{ {
var tcs = new TaskCompletionSource(); var tcs = new TaskCompletionSource();
tile.Image.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () => async void callback()
{ {
try try
{ {
@ -80,7 +80,13 @@ namespace MapControl
{ {
tcs.TrySetException(ex); tcs.TrySetException(ex);
} }
}); }
if (!tile.Image.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, callback))
{
tile.Pending = true;
tcs.TrySetResult();
}
return tcs.Task; return tcs.Task;
} }