Abstract classes Tile, TileSource

This commit is contained in:
ClemensFischer 2025-11-13 15:32:01 +01:00
parent 20e4fcce75
commit cb8fff0dd1
14 changed files with 95 additions and 98 deletions

View file

@ -16,7 +16,7 @@ namespace MapControl.MBTiles
/// <summary> /// <summary>
/// MapTileLayer that uses an MBTiles SQLite Database. See https://wiki.openstreetmap.org/wiki/MBTiles. /// MapTileLayer that uses an MBTiles SQLite Database. See https://wiki.openstreetmap.org/wiki/MBTiles.
/// </summary> /// </summary>
public partial class MBTileLayer : MapTileLayer public class MBTileLayer : MapTileLayer
{ {
private static ILogger logger; private static ILogger logger;
private static ILogger Logger => logger ??= ImageLoader.LoggerFactory?.CreateLogger<MBTileLayer>(); private static ILogger Logger => logger ??= ImageLoader.LoggerFactory?.CreateLogger<MBTileLayer>();

View file

@ -15,7 +15,7 @@ using ImageSource = Avalonia.Media.IImage;
namespace MapControl.MBTiles namespace MapControl.MBTiles
{ {
public sealed partial class MBTileSource : TileSource, IDisposable public sealed class MBTileSource : TileSource, IDisposable
{ {
private static ILogger logger; private static ILogger logger;
private static ILogger Logger => logger ??= ImageLoader.LoggerFactory?.CreateLogger<MBTileSource>(); private static ILogger Logger => logger ??= ImageLoader.LoggerFactory?.CreateLogger<MBTileSource>();
@ -24,6 +24,8 @@ namespace MapControl.MBTiles
public IDictionary<string, string> Metadata { get; } = new Dictionary<string, string>(); public IDictionary<string, string> Metadata { get; } = new Dictionary<string, string>();
public override bool Cacheable => false;
public async Task OpenAsync(string file) public async Task OpenAsync(string file)
{ {
Close(); Close();
@ -85,5 +87,10 @@ namespace MapControl.MBTiles
return image; return image;
} }
public override Uri GetUri(int zoomLevel, int column, int row)
{
throw new NotSupportedException();
}
} }
} }

View file

@ -1,5 +1,6 @@
using Avalonia; using Avalonia;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
@ -8,9 +9,12 @@ using System.Threading.Tasks;
namespace MapControl namespace MapControl
{ {
public partial class Tile public class ImageTile(int zoomLevel, int x, int y, int columnCount)
: Tile(zoomLevel, x, y, columnCount)
{ {
public async Task LoadImageAsync(Func<Task<IImage>> loadImageFunc) public Image Image { get; } = new Image { Stretch = Stretch.Fill };
public override async Task LoadImageAsync(Func<Task<IImage>> loadImageFunc)
{ {
var image = await loadImageFunc().ConfigureAwait(false); var image = await loadImageFunc().ConfigureAwait(false);

View file

@ -4,7 +4,7 @@ using System.Text;
namespace MapControl namespace MapControl
{ {
public class BoundingBoxTileSource : TileSource public class BoundingBoxTileSource : UriTileSource
{ {
public override Uri GetUri(int zoomLevel, int column, int row) public override Uri GetUri(int zoomLevel, int column, int row)
{ {

View file

@ -3,12 +3,12 @@ using System.Linq;
namespace MapControl namespace MapControl
{ {
public partial class TileCollection : List<Tile> public partial class ImageTileList : List<ImageTile>
{ {
/// <summary> /// <summary>
/// Adds existing Tiles from the source collection or newly created Tiles to fill the specified tile matrix. /// Adds existing ImageTile from the source collection or newly created ImageTile to fill the specified tile matrix.
/// </summary> /// </summary>
public void FillMatrix(TileCollection source, int zoomLevel, int xMin, int yMin, int xMax, int yMax, int columnCount) public void FillMatrix(ImageTileList source, int zoomLevel, int xMin, int yMin, int xMax, int yMax, int columnCount)
{ {
for (var y = yMin; y <= yMax; y++) for (var y = yMin; y <= yMax; y++)
{ {
@ -18,7 +18,7 @@ namespace MapControl
if (tile == null) if (tile == null)
{ {
tile = new Tile(zoomLevel, x, y, columnCount); tile = new ImageTile(zoomLevel, x, y, columnCount);
var equivalentTile = source.FirstOrDefault( var equivalentTile = source.FirstOrDefault(
t => t.Image.Source != null && t.ZoomLevel == tile.ZoomLevel && t.Column == tile.Column && t.Row == tile.Row); t => t.Image.Source != null && t.ZoomLevel == tile.ZoomLevel && t.Column == tile.Column && t.Row == tile.Row);

View file

@ -43,7 +43,7 @@ namespace MapControl
/// </summary> /// </summary>
public static MapTileLayer OpenStreetMapTileLayer => new() public static MapTileLayer OpenStreetMapTileLayer => new()
{ {
TileSource = new TileSource { UriTemplate = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" }, TileSource = TileSource.Parse("https://tile.openstreetmap.org/{z}/{x}/{y}.png"),
SourceName = "OpenStreetMap", SourceName = "OpenStreetMap",
Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)" Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)"
}; };
@ -52,7 +52,7 @@ namespace MapControl
public TileMatrix TileMatrix { get; private set; } public TileMatrix TileMatrix { get; private set; }
public TileCollection Tiles { get; private set; } = []; public ImageTileList Tiles { get; private set; } = [];
/// <summary> /// <summary>
/// Minimum zoom level supported by the MapTileLayer. Default value is 0. /// Minimum zoom level supported by the MapTileLayer. Default value is 0.
@ -178,7 +178,7 @@ namespace MapControl
private void UpdateTiles(bool reset) private void UpdateTiles(bool reset)
{ {
var tiles = new TileCollection(); var tiles = new ImageTileList();
if (TileSource != null && TileMatrix != null) if (TileSource != null && TileMatrix != null)
{ {

View file

@ -1,20 +1,18 @@
#if WPF using System;
using System.Windows.Controls; using System.Threading.Tasks;
#if WPF
using System.Windows.Media; using System.Windows.Media;
#elif UWP #elif UWP
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media;
#elif WINUI #elif WINUI
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
#elif AVALONIA #elif AVALONIA
using Avalonia.Controls; using ImageSource = Avalonia.Media.IImage;
using Avalonia.Media;
#endif #endif
namespace MapControl namespace MapControl
{ {
public partial class Tile(int zoomLevel, int x, int y, int columnCount) : ITile public abstract class Tile(int zoomLevel, int x, int y, int columnCount)
{ {
public int ZoomLevel { get; } = zoomLevel; public int ZoomLevel { get; } = zoomLevel;
public int X { get; } = x; public int X { get; } = x;
@ -22,6 +20,10 @@ namespace MapControl
public int Column { get; } = ((x % columnCount) + columnCount) % columnCount; public int Column { get; } = ((x % columnCount) + columnCount) % columnCount;
public int Row => Y; public int Row => Y;
public bool IsPending { get; set; } = true; public bool IsPending { get; set; } = true;
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
/// <summary>
/// Runs a tile image download Task and marshals the result to the UI thread.
/// </summary>
public abstract Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc);
} }
} }

View file

@ -7,61 +7,16 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
#if WPF
using System.Windows.Media;
#elif UWP
using Windows.UI.Xaml.Media;
#elif WINUI
using Microsoft.UI.Xaml.Media;
#elif AVALONIA
using ImageSource = Avalonia.Media.IImage;
#endif
namespace MapControl namespace MapControl
{ {
public interface ITile
{
int ZoomLevel { get; }
int Column { get; }
int Row { get; }
bool IsPending { get; set; }
/// <summary>
/// Runs a tile image download Task and marshals the result to the UI thread.
/// </summary>
Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc);
}
public interface ITileSource
{
/// <summary>
/// Indicates whether tile images from this source should be cached.
/// </summary>
bool Cacheable { get; }
/// <summary>
/// Gets the image Uri for the specified zoom level and tile indices.
/// </summary>
Uri GetUri(int zoomLevel, int column, int row);
/// <summary>
/// Loads a tile image whithout caching.
/// </summary>
Task<ImageSource> LoadImageAsync(int zoomLevel, int column, int row);
/// <summary>
/// Loads a cacheable tile image from an encoded frame buffer.
/// </summary>
Task<ImageSource> LoadImageAsync(byte[] buffer);
}
public interface ITileImageLoader public interface ITileImageLoader
{ {
/// <summary> /// <summary>
/// Loads all pending tiles from the tiles collection. /// Loads all pending tiles from the tiles collection.
/// Tile image caching is enabled when tileSource.UriFormat starts with "http" and cacheName is a non-empty string. /// Tile image caching is enabled when tileSource.UriFormat starts with "http" and cacheName is a non-empty string.
/// </summary> /// </summary>
void BeginLoadTiles(IEnumerable<ITile> tiles, ITileSource tileSource, string cacheName, IProgress<double> progress); void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress);
/// <summary> /// <summary>
/// Cancels a potentially ongoing tile loading task. /// Cancels a potentially ongoing tile loading task.
@ -115,11 +70,11 @@ namespace MapControl
/// </summary> /// </summary>
public static int MaxLoadTasks { get; set; } = 4; public static int MaxLoadTasks { get; set; } = 4;
private readonly Queue<ITile> tileQueue = new(); private readonly Queue<Tile> tileQueue = new();
private int tileCount; private int tileCount;
private int taskCount; private int taskCount;
public void BeginLoadTiles(IEnumerable<ITile> tiles, ITileSource tileSource, string cacheName, IProgress<double> progress) public void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress)
{ {
if (Cache == null || !tileSource.Cacheable) if (Cache == null || !tileSource.Cacheable)
{ {
@ -158,9 +113,9 @@ namespace MapControl
} }
} }
private async Task LoadTilesFromQueue(ITileSource tileSource, string cacheName, IProgress<double> progress) private async Task LoadTilesFromQueue(TileSource tileSource, string cacheName, IProgress<double> progress)
{ {
bool TryDequeueTile(out ITile tile) bool TryDequeueTile(out Tile tile)
{ {
lock (tileQueue) lock (tileQueue)
{ {
@ -178,7 +133,7 @@ namespace MapControl
return false; return false;
} }
while (TryDequeueTile(out ITile tile)) while (TryDequeueTile(out Tile tile))
{ {
tile.IsPending = false; tile.IsPending = false;
@ -218,7 +173,7 @@ namespace MapControl
} }
} }
private static async Task<byte[]> LoadCachedBuffer(ITile tile, Uri uri, string cacheName) private static async Task<byte[]> LoadCachedBuffer(Tile tile, Uri uri, string cacheName)
{ {
var extension = Path.GetExtension(uri.LocalPath).ToLower(); var extension = Path.GetExtension(uri.LocalPath).ToLower();

View file

@ -192,7 +192,7 @@ namespace MapControl
protected bool IsBaseMapLayer => parentMap != null && parentMap.Children.Count > 0 && parentMap.Children[0] == this; protected bool IsBaseMapLayer => parentMap != null && parentMap.Children.Count > 0 && parentMap.Children[0] == this;
protected void BeginLoadTiles(IEnumerable<ITile> tiles, string cacheName) protected void BeginLoadTiles(IEnumerable<Tile> tiles, string cacheName)
{ {
if (TileSource != null && tiles != null && tiles.Any(tile => tile.IsPending)) if (TileSource != null && tiles != null && tiles.Any(tile => tile.IsPending))
{ {

View file

@ -21,7 +21,41 @@ namespace MapControl
#else #else
[System.ComponentModel.TypeConverter(typeof(TileSourceConverter))] [System.ComponentModel.TypeConverter(typeof(TileSourceConverter))]
#endif #endif
public class TileSource : ITileSource public abstract class TileSource
{
/// <summary>
/// Indicates whether tile images from this source should be cached.
/// </summary>
public abstract bool Cacheable { get; }
/// <summary>
/// Gets the image Uri for the specified zoom level and tile indices.
/// </summary>
public abstract Uri GetUri(int zoomLevel, int column, int row);
/// <summary>
/// Loads a tile image whithout caching.
/// </summary>
public abstract Task<ImageSource> LoadImageAsync(int zoomLevel, int column, int row);
/// <summary>
/// Loads a cacheable tile image from an encoded frame buffer.
/// </summary>
public virtual Task<ImageSource> LoadImageAsync(byte[] buffer)
{
return ImageLoader.LoadImageAsync(buffer);
}
/// <summary>
/// Creates a TileSource instance from an Uri template string.
/// </summary>
public static TileSource Parse(string uriTemplate)
{
return new UriTileSource { UriTemplate = uriTemplate };
}
}
public class UriTileSource : TileSource
{ {
private string uriTemplate; private string uriTemplate;
@ -44,9 +78,9 @@ namespace MapControl
public string[] Subdomains { get; set; } public string[] Subdomains { get; set; }
public bool Cacheable => UriTemplate != null && UriTemplate.StartsWith("http"); public override bool Cacheable => UriTemplate != null && UriTemplate.StartsWith("http");
public virtual Uri GetUri(int zoomLevel, int column, int row) public override Uri GetUri(int zoomLevel, int column, int row)
{ {
Uri uri = null; Uri uri = null;
@ -69,33 +103,20 @@ namespace MapControl
return uri; return uri;
} }
public virtual Task<ImageSource> LoadImageAsync(int zoomLevel, int column, int row) public override Task<ImageSource> LoadImageAsync(int zoomLevel, int column, int row)
{ {
var uri = GetUri(zoomLevel, column, row); var uri = GetUri(zoomLevel, column, row);
return uri != null ? ImageLoader.LoadImageAsync(uri) : Task.FromResult((ImageSource)null); return uri != null ? ImageLoader.LoadImageAsync(uri) : Task.FromResult((ImageSource)null);
} }
public virtual Task<ImageSource> LoadImageAsync(byte[] buffer)
{
return ImageLoader.LoadImageAsync(buffer);
}
public override string ToString() public override string ToString()
{ {
return UriTemplate; return UriTemplate;
} }
/// <summary>
/// Creates a TileSource instance from an Uri template string.
/// </summary>
public static TileSource Parse(string uriTemplate)
{
return new TileSource { UriTemplate = uriTemplate };
}
} }
public class TmsTileSource : TileSource public class TmsTileSource : UriTileSource
{ {
public override Uri GetUri(int zoomLevel, int column, int row) public override Uri GetUri(int zoomLevel, int column, int row)
{ {

View file

@ -35,7 +35,7 @@ namespace MapControl
public TileMatrix TileMatrix { get; private set; } public TileMatrix TileMatrix { get; private set; }
public TileCollection Tiles { get; private set; } = []; public ImageTileList Tiles { get; private set; } = [];
public void UpdateRenderTransform(ViewTransform viewTransform) public void UpdateRenderTransform(ViewTransform viewTransform)
{ {
@ -85,7 +85,7 @@ namespace MapControl
TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax); TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax);
var tiles = new TileCollection(); var tiles = new ImageTileList();
tiles.FillMatrix(Tiles, TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax, WmtsTileMatrix.MatrixWidth); tiles.FillMatrix(Tiles, TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax, WmtsTileMatrix.MatrixWidth);
Tiles = tiles; Tiles = tiles;

View file

@ -3,7 +3,7 @@ using System.Text;
namespace MapControl namespace MapControl
{ {
public class WmtsTileSource : TileSource public class WmtsTileSource : UriTileSource
{ {
public WmtsTileMatrixSet TileMatrixSet { get; set; } public WmtsTileMatrixSet TileMatrixSet { get; set; }

View file

@ -8,9 +8,12 @@ using System.Windows.Media.Imaging;
namespace MapControl namespace MapControl
{ {
public partial class Tile public class ImageTile(int zoomLevel, int x, int y, int columnCount)
: Tile(zoomLevel, x, y, columnCount)
{ {
public async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc) public Image Image { get; } = new Image { Stretch = Stretch.Fill };
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
{ {
var image = await loadImageFunc().ConfigureAwait(false); var image = await loadImageFunc().ConfigureAwait(false);

View file

@ -3,12 +3,14 @@ using System.Threading.Tasks;
#if UWP #if UWP
using Windows.UI.Core; using Windows.UI.Core;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation; using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Media.Imaging; using Windows.UI.Xaml.Media.Imaging;
#else #else
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
@ -16,9 +18,12 @@ using Microsoft.UI.Xaml.Media.Imaging;
namespace MapControl namespace MapControl
{ {
public partial class Tile public class ImageTile(int zoomLevel, int x, int y, int columnCount)
: Tile(zoomLevel, x, y, columnCount)
{ {
public async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc) public Image Image { get; } = new Image { Stretch = Stretch.Fill };
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
{ {
var tcs = new TaskCompletionSource<object>(); var tcs = new TaskCompletionSource<object>();