diff --git a/Caches/FileDbCache/FileDbCache.cs b/Caches/FileDbCache/FileDbCache.cs index c9cf40ad..30cc9781 100644 --- a/Caches/FileDbCache/FileDbCache.cs +++ b/Caches/FileDbCache/FileDbCache.cs @@ -7,200 +7,199 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace MapControl.Caching +namespace MapControl.Caching; + +public class FileDbCacheOptions : IOptions { - public class FileDbCacheOptions : IOptions + public FileDbCacheOptions Value => this; + + public string Path { get; set; } + + public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromHours(1); +} + +/// +/// IDistributedCache implementation based on FileDb, https://github.com/eztools-software/FileDb. +/// +public sealed class FileDbCache : IDistributedCache, IDisposable +{ + private const string KeyField = "Key"; + private const string ValueField = "Value"; + private const string ExpiresField = "Expires"; + + private readonly FileDb fileDb = new FileDb { AutoFlush = true }; + private readonly Timer timer; + private readonly ILogger logger; + + public FileDbCache(string path, ILoggerFactory loggerFactory = null) + : this(new FileDbCacheOptions { Path = path }, loggerFactory) { - public FileDbCacheOptions Value => this; - - public string Path { get; set; } - - public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromHours(1); } - /// - /// IDistributedCache implementation based on FileDb, https://github.com/eztools-software/FileDb. - /// - public sealed class FileDbCache : IDistributedCache, IDisposable + public FileDbCache(IOptions optionsAccessor, ILoggerFactory loggerFactory = null) + : this(optionsAccessor.Value, loggerFactory) { - private const string KeyField = "Key"; - private const string ValueField = "Value"; - private const string ExpiresField = "Expires"; + } - private readonly FileDb fileDb = new FileDb { AutoFlush = true }; - private readonly Timer timer; - private readonly ILogger logger; + public FileDbCache(FileDbCacheOptions options, ILoggerFactory loggerFactory = null) + { + var path = options.Path; - public FileDbCache(string path, ILoggerFactory loggerFactory = null) - : this(new FileDbCacheOptions { Path = path }, loggerFactory) + if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(Path.GetExtension(path))) { + path = Path.Combine(path ?? "", "TileCache.fdb"); } - public FileDbCache(IOptions optionsAccessor, ILoggerFactory loggerFactory = null) - : this(optionsAccessor.Value, loggerFactory) + logger = loggerFactory?.CreateLogger(); + + try { + fileDb.Open(path); + + logger?.LogInformation("Opened database {path}", path); } - - public FileDbCache(FileDbCacheOptions options, ILoggerFactory loggerFactory = null) + catch { - var path = options.Path; - - if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(Path.GetExtension(path))) + if (File.Exists(path)) { - path = Path.Combine(path ?? "", "TileCache.fdb"); + File.Delete(path); + } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); } - logger = loggerFactory?.CreateLogger(); + fileDb.Create(path, new Field[] + { + new Field(KeyField, DataTypeEnum.String) { IsPrimaryKey = true }, + new Field(ValueField, DataTypeEnum.Byte) { IsArray = true }, + new Field(ExpiresField, DataTypeEnum.DateTime) + }); + + logger?.LogInformation("Created database {path}", path); + } + + if (options.ExpirationScanFrequency > TimeSpan.Zero) + { + timer = new Timer(_ => DeleteExpiredItems(), null, TimeSpan.Zero, options.ExpirationScanFrequency); + } + } + + public void Dispose() + { + timer?.Dispose(); + fileDb.Dispose(); + } + + public byte[] Get(string key) + { + byte[] value = null; + + if (!string.IsNullOrEmpty(key)) + { + try + { + var record = fileDb.GetRecordByKey(key, new string[] { ValueField, ExpiresField }, false); + + if (record != null && (DateTime)record[1] > DateTime.UtcNow) + { + value = (byte[])record[0]; + } + } + catch (Exception ex) + { + logger?.LogError(ex, "Get({key})", key); + } + } + + return value; + } + + public Task GetAsync(string key, CancellationToken token = default) + { + return Task.FromResult(Get(key)); + } + + public void Set(string key, byte[] value, DistributedCacheEntryOptions options) + { + if (!string.IsNullOrEmpty(key) && value != null && options != null) + { + var expiration = options.AbsoluteExpiration.HasValue + ? options.AbsoluteExpiration.Value.UtcDateTime + : DateTime.UtcNow.Add(options.AbsoluteExpirationRelativeToNow ?? options.SlidingExpiration ?? TimeSpan.FromDays(1)); + + var fieldValues = new FieldValues(3) + { + { ValueField, value }, + { ExpiresField, expiration } + }; try { - fileDb.Open(path); - - logger?.LogInformation("Opened database {path}", path); - } - catch - { - if (File.Exists(path)) + if (fileDb.GetRecordByKey(key, new string[0], false) != null) { - File.Delete(path); + fileDb.UpdateRecordByKey(key, fieldValues); } else { - Directory.CreateDirectory(Path.GetDirectoryName(path)); - } - - fileDb.Create(path, new Field[] - { - new Field(KeyField, DataTypeEnum.String) { IsPrimaryKey = true }, - new Field(ValueField, DataTypeEnum.Byte) { IsArray = true }, - new Field(ExpiresField, DataTypeEnum.DateTime) - }); - - logger?.LogInformation("Created database {path}", path); - } - - if (options.ExpirationScanFrequency > TimeSpan.Zero) - { - timer = new Timer(_ => DeleteExpiredItems(), null, TimeSpan.Zero, options.ExpirationScanFrequency); - } - } - - public void Dispose() - { - timer?.Dispose(); - fileDb.Dispose(); - } - - public byte[] Get(string key) - { - byte[] value = null; - - if (!string.IsNullOrEmpty(key)) - { - try - { - var record = fileDb.GetRecordByKey(key, new string[] { ValueField, ExpiresField }, false); - - if (record != null && (DateTime)record[1] > DateTime.UtcNow) - { - value = (byte[])record[0]; - } - } - catch (Exception ex) - { - logger?.LogError(ex, "Get({key})", key); + fieldValues.Add(KeyField, key); + fileDb.AddRecord(fieldValues); } } - - return value; - } - - public Task GetAsync(string key, CancellationToken token = default) - { - return Task.FromResult(Get(key)); - } - - public void Set(string key, byte[] value, DistributedCacheEntryOptions options) - { - if (!string.IsNullOrEmpty(key) && value != null && options != null) + catch (Exception ex) { - var expiration = options.AbsoluteExpiration.HasValue - ? options.AbsoluteExpiration.Value.UtcDateTime - : DateTime.UtcNow.Add(options.AbsoluteExpirationRelativeToNow ?? options.SlidingExpiration ?? TimeSpan.FromDays(1)); - - var fieldValues = new FieldValues(3) - { - { ValueField, value }, - { ExpiresField, expiration } - }; - - try - { - if (fileDb.GetRecordByKey(key, new string[0], false) != null) - { - fileDb.UpdateRecordByKey(key, fieldValues); - } - else - { - fieldValues.Add(KeyField, key); - fileDb.AddRecord(fieldValues); - } - } - catch (Exception ex) - { - logger?.LogError(ex, "Set({key})", key); - } - } - } - - public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) - { - Set(key, value, options); - - return Task.CompletedTask; - } - - public void Refresh(string key) - { - } - - public Task RefreshAsync(string key, CancellationToken token = default) - { - return Task.CompletedTask; - } - - public void Remove(string key) - { - if (!string.IsNullOrEmpty(key)) - { - try - { - fileDb.DeleteRecordByKey(key); - } - catch (Exception ex) - { - logger?.LogError(ex, "Remove({key})", key); - } - } - } - - public Task RemoveAsync(string key, CancellationToken token = default) - { - Remove(key); - - return Task.CompletedTask; - } - - public void DeleteExpiredItems() - { - var deletedItemsCount = fileDb.DeleteRecords(new FilterExpression(ExpiresField, DateTime.UtcNow, ComparisonOperatorEnum.LessThanOrEqual)); - - if (deletedItemsCount > 0) - { - fileDb.Clean(); - - logger?.LogInformation("Deleted {count} expired items", deletedItemsCount); + logger?.LogError(ex, "Set({key})", key); } } } + + public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) + { + Set(key, value, options); + + return Task.CompletedTask; + } + + public void Refresh(string key) + { + } + + public Task RefreshAsync(string key, CancellationToken token = default) + { + return Task.CompletedTask; + } + + public void Remove(string key) + { + if (!string.IsNullOrEmpty(key)) + { + try + { + fileDb.DeleteRecordByKey(key); + } + catch (Exception ex) + { + logger?.LogError(ex, "Remove({key})", key); + } + } + } + + public Task RemoveAsync(string key, CancellationToken token = default) + { + Remove(key); + + return Task.CompletedTask; + } + + public void DeleteExpiredItems() + { + var deletedItemsCount = fileDb.DeleteRecords(new FilterExpression(ExpiresField, DateTime.UtcNow, ComparisonOperatorEnum.LessThanOrEqual)); + + if (deletedItemsCount > 0) + { + fileDb.Clean(); + + logger?.LogInformation("Deleted {count} expired items", deletedItemsCount); + } + } } diff --git a/Caches/FileDbCache/FileDbCache.csproj b/Caches/FileDbCache/FileDbCache.csproj index c0eb8668..874f13f5 100644 --- a/Caches/FileDbCache/FileDbCache.csproj +++ b/Caches/FileDbCache/FileDbCache.csproj @@ -1,6 +1,7 @@  netstandard2.0 + 14.0 MapControl.Caching XAML Map Control FileDbCache Library $(GeneratePackage) diff --git a/Caches/SQLiteCache/SQLiteCache.cs b/Caches/SQLiteCache/SQLiteCache.cs index 28ecf52e..aec27c0f 100644 --- a/Caches/SQLiteCache/SQLiteCache.cs +++ b/Caches/SQLiteCache/SQLiteCache.cs @@ -7,250 +7,249 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace MapControl.Caching +namespace MapControl.Caching; + +public class SQLiteCacheOptions : IOptions { - public class SQLiteCacheOptions : IOptions + public SQLiteCacheOptions Value => this; + + public string Path { get; set; } + + public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromHours(1); +} + +/// +/// IDistributedCache implementation based on System.Data.SQLite, https://system.data.sqlite.org/. +/// +public sealed class SQLiteCache : IDistributedCache, IDisposable +{ + private readonly SQLiteConnection connection; + private readonly Timer timer; + private readonly ILogger logger; + + public SQLiteCache(string path, ILoggerFactory loggerFactory = null) + : this(new SQLiteCacheOptions { Path = path }, loggerFactory) { - public SQLiteCacheOptions Value => this; - - public string Path { get; set; } - - public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromHours(1); } - /// - /// IDistributedCache implementation based on System.Data.SQLite, https://system.data.sqlite.org/. - /// - public sealed class SQLiteCache : IDistributedCache, IDisposable + public SQLiteCache(IOptions optionsAccessor, ILoggerFactory loggerFactory = null) + : this(optionsAccessor.Value, loggerFactory) { - private readonly SQLiteConnection connection; - private readonly Timer timer; - private readonly ILogger logger; + } - public SQLiteCache(string path, ILoggerFactory loggerFactory = null) - : this(new SQLiteCacheOptions { Path = path }, loggerFactory) + public SQLiteCache(SQLiteCacheOptions options, ILoggerFactory loggerFactory = null) + { + var path = options.Path; + + if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(Path.GetExtension(path))) { + path = Path.Combine(path ?? "", "TileCache.sqlite"); } - public SQLiteCache(IOptions optionsAccessor, ILoggerFactory loggerFactory = null) - : this(optionsAccessor.Value, loggerFactory) + connection = new SQLiteConnection("Data Source=" + path); + connection.Open(); + + using (var command = new SQLiteCommand("pragma journal_mode=wal", connection)) { + command.ExecuteNonQuery(); } - public SQLiteCache(SQLiteCacheOptions options, ILoggerFactory loggerFactory = null) + using (var command = new SQLiteCommand("create table if not exists items (key text primary key, expiration integer, buffer blob)", connection)) { - var path = options.Path; + command.ExecuteNonQuery(); + } - if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(Path.GetExtension(path))) + logger = loggerFactory?.CreateLogger(); + logger?.LogInformation("Opened database {path}", path); + + if (options.ExpirationScanFrequency > TimeSpan.Zero) + { + timer = new Timer(_ => DeleteExpiredItems(), null, TimeSpan.Zero, options.ExpirationScanFrequency); + } + } + + public void Dispose() + { + timer?.Dispose(); + connection.Dispose(); + } + + public byte[] Get(string key) + { + byte[] value = null; + + if (!string.IsNullOrEmpty(key)) + { + try { - path = Path.Combine(path ?? "", "TileCache.sqlite"); + using (var command = GetItemCommand(key)) + { + value = (byte[])command.ExecuteScalar(); + } } - - connection = new SQLiteConnection("Data Source=" + path); - connection.Open(); - - using (var command = new SQLiteCommand("pragma journal_mode=wal", connection)) + catch (Exception ex) { - command.ExecuteNonQuery(); - } - - using (var command = new SQLiteCommand("create table if not exists items (key text primary key, expiration integer, buffer blob)", connection)) - { - command.ExecuteNonQuery(); - } - - logger = loggerFactory?.CreateLogger(); - logger?.LogInformation("Opened database {path}", path); - - if (options.ExpirationScanFrequency > TimeSpan.Zero) - { - timer = new Timer(_ => DeleteExpiredItems(), null, TimeSpan.Zero, options.ExpirationScanFrequency); + logger?.LogError(ex, "Get({key})", key); } } - public void Dispose() - { - timer?.Dispose(); - connection.Dispose(); - } + return value; + } - public byte[] Get(string key) - { - byte[] value = null; + public async Task GetAsync(string key, CancellationToken token = default) + { + byte[] value = null; - if (!string.IsNullOrEmpty(key)) + if (!string.IsNullOrEmpty(key)) + { + try { - try + using (var command = GetItemCommand(key)) { - using (var command = GetItemCommand(key)) - { - value = (byte[])command.ExecuteScalar(); - } - } - catch (Exception ex) - { - logger?.LogError(ex, "Get({key})", key); + value = (byte[])await command.ExecuteScalarAsync(); } } - - return value; - } - - public async Task GetAsync(string key, CancellationToken token = default) - { - byte[] value = null; - - if (!string.IsNullOrEmpty(key)) + catch (Exception ex) { - try - { - using (var command = GetItemCommand(key)) - { - value = (byte[])await command.ExecuteScalarAsync(); - } - } - catch (Exception ex) - { - logger?.LogError(ex, "GetAsync({key})", key); - } - } - - return value; - } - - public void Set(string key, byte[] value, DistributedCacheEntryOptions options) - { - if (!string.IsNullOrEmpty(key) && value != null && options != null) - { - try - { - using (var command = SetItemCommand(key, value, options)) - { - command.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - logger?.LogError(ex, "Set({key})", key); - } + logger?.LogError(ex, "GetAsync({key})", key); } } - public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) + return value; + } + + public void Set(string key, byte[] value, DistributedCacheEntryOptions options) + { + if (!string.IsNullOrEmpty(key) && value != null && options != null) { - if (!string.IsNullOrEmpty(key) && value != null && options != null) + try { - try - { - using (var command = SetItemCommand(key, value, options)) - { - await command.ExecuteNonQueryAsync(token); - } - } - catch (Exception ex) - { - logger?.LogError(ex, "SetAsync({key})", key); - } - } - } - - public void Refresh(string key) - { - } - - public Task RefreshAsync(string key, CancellationToken token = default) - { - return Task.CompletedTask; - } - - public void Remove(string key) - { - if (!string.IsNullOrEmpty(key)) - { - try - { - using (var command = DeleteItemCommand(key)) - { - command.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - logger?.LogError(ex, "Remove({key})", key); - } - } - } - - public async Task RemoveAsync(string key, CancellationToken token = default) - { - if (!string.IsNullOrEmpty(key)) - { - try - { - using (var command = DeleteItemCommand(key)) - { - await command.ExecuteNonQueryAsync(); - } - } - catch (Exception ex) - { - logger?.LogError(ex, "RemoveAsync({key})", key); - } - } - } - - public void DeleteExpiredItems() - { - long deletedItemsCount; - - using (var command = DeleteExpiredItemsCommand()) - { - deletedItemsCount = (long)command.ExecuteScalar(); - } - - if (deletedItemsCount > 0) - { - using (var command = new SQLiteCommand("vacuum", connection)) + using (var command = SetItemCommand(key, value, options)) { command.ExecuteNonQuery(); } - - logger?.LogInformation("Deleted {count} expired items", deletedItemsCount); + } + catch (Exception ex) + { + logger?.LogError(ex, "Set({key})", key); } } + } - private SQLiteCommand GetItemCommand(string key) + public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) + { + if (!string.IsNullOrEmpty(key) && value != null && options != null) { - var command = new SQLiteCommand("select buffer from items where key = @key and expiration > @exp", connection); - command.Parameters.AddWithValue("@key", key); - command.Parameters.AddWithValue("@exp", DateTimeOffset.UtcNow.Ticks); - return command; - } - - private SQLiteCommand SetItemCommand(string key, byte[] buffer, DistributedCacheEntryOptions options) - { - var expiration = options.AbsoluteExpiration ?? - DateTimeOffset.UtcNow.Add(options.AbsoluteExpirationRelativeToNow ?? options.SlidingExpiration ?? TimeSpan.FromDays(1)); - - var command = new SQLiteCommand("insert or replace into items (key, expiration, buffer) values (@key, @exp, @buf)", connection); - command.Parameters.AddWithValue("@key", key); - command.Parameters.AddWithValue("@exp", expiration.UtcTicks); - command.Parameters.AddWithValue("@buf", buffer); - return command; - } - - private SQLiteCommand DeleteItemCommand(string key) - { - var command = new SQLiteCommand("delete from items where key = @key", connection); - command.Parameters.AddWithValue("@key", key); - return command; - } - - private SQLiteCommand DeleteExpiredItemsCommand() - { - var command = new SQLiteCommand("delete from items where expiration <= @exp; select changes()", connection); - command.Parameters.AddWithValue("@exp", DateTimeOffset.UtcNow.Ticks); - return command; + try + { + using (var command = SetItemCommand(key, value, options)) + { + await command.ExecuteNonQueryAsync(token); + } + } + catch (Exception ex) + { + logger?.LogError(ex, "SetAsync({key})", key); + } } } + + public void Refresh(string key) + { + } + + public Task RefreshAsync(string key, CancellationToken token = default) + { + return Task.CompletedTask; + } + + public void Remove(string key) + { + if (!string.IsNullOrEmpty(key)) + { + try + { + using (var command = DeleteItemCommand(key)) + { + command.ExecuteNonQuery(); + } + } + catch (Exception ex) + { + logger?.LogError(ex, "Remove({key})", key); + } + } + } + + public async Task RemoveAsync(string key, CancellationToken token = default) + { + if (!string.IsNullOrEmpty(key)) + { + try + { + using (var command = DeleteItemCommand(key)) + { + await command.ExecuteNonQueryAsync(); + } + } + catch (Exception ex) + { + logger?.LogError(ex, "RemoveAsync({key})", key); + } + } + } + + public void DeleteExpiredItems() + { + long deletedItemsCount; + + using (var command = DeleteExpiredItemsCommand()) + { + deletedItemsCount = (long)command.ExecuteScalar(); + } + + if (deletedItemsCount > 0) + { + using (var command = new SQLiteCommand("vacuum", connection)) + { + command.ExecuteNonQuery(); + } + + logger?.LogInformation("Deleted {count} expired items", deletedItemsCount); + } + } + + private SQLiteCommand GetItemCommand(string key) + { + var command = new SQLiteCommand("select buffer from items where key = @key and expiration > @exp", connection); + command.Parameters.AddWithValue("@key", key); + command.Parameters.AddWithValue("@exp", DateTimeOffset.UtcNow.Ticks); + return command; + } + + private SQLiteCommand SetItemCommand(string key, byte[] buffer, DistributedCacheEntryOptions options) + { + var expiration = options.AbsoluteExpiration ?? + DateTimeOffset.UtcNow.Add(options.AbsoluteExpirationRelativeToNow ?? options.SlidingExpiration ?? TimeSpan.FromDays(1)); + + var command = new SQLiteCommand("insert or replace into items (key, expiration, buffer) values (@key, @exp, @buf)", connection); + command.Parameters.AddWithValue("@key", key); + command.Parameters.AddWithValue("@exp", expiration.UtcTicks); + command.Parameters.AddWithValue("@buf", buffer); + return command; + } + + private SQLiteCommand DeleteItemCommand(string key) + { + var command = new SQLiteCommand("delete from items where key = @key", connection); + command.Parameters.AddWithValue("@key", key); + return command; + } + + private SQLiteCommand DeleteExpiredItemsCommand() + { + var command = new SQLiteCommand("delete from items where expiration <= @exp; select changes()", connection); + command.Parameters.AddWithValue("@exp", DateTimeOffset.UtcNow.Ticks); + return command; + } } diff --git a/Caches/SQLiteCache/SQLiteCache.csproj b/Caches/SQLiteCache/SQLiteCache.csproj index 0529407b..17627f5d 100644 --- a/Caches/SQLiteCache/SQLiteCache.csproj +++ b/Caches/SQLiteCache/SQLiteCache.csproj @@ -1,6 +1,7 @@  netstandard2.0 + 14.0 MapControl.Caching XAML Map Control SQLiteCache Library $(GeneratePackage) diff --git a/MBTiles/Shared/MBTileLayer.cs b/MBTiles/Shared/MBTileLayer.cs index 74b76a40..076e69f0 100644 --- a/MBTiles/Shared/MBTileLayer.cs +++ b/MBTiles/Shared/MBTileLayer.cs @@ -11,86 +11,85 @@ using Microsoft.UI.Xaml; using DependencyProperty = Avalonia.AvaloniaProperty; #endif -namespace MapControl.MBTiles +namespace MapControl.MBTiles; + +/// +/// MapTileLayer that uses an MBTiles SQLite Database. See https://wiki.openstreetmap.org/wiki/MBTiles. +/// +public class MBTileLayer : MapTileLayer { - /// - /// MapTileLayer that uses an MBTiles SQLite Database. See https://wiki.openstreetmap.org/wiki/MBTiles. - /// - public class MBTileLayer : MapTileLayer + private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(); + + public static readonly DependencyProperty FileProperty = + DependencyPropertyHelper.Register(nameof(File), null, + async (layer, oldValue, newValue) => await layer.FilePropertyChanged(newValue)); + + public string File { - private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(); + get => (string)GetValue(FileProperty); + set => SetValue(FileProperty, value); + } - public static readonly DependencyProperty FileProperty = - DependencyPropertyHelper.Register(nameof(File), null, - async (layer, oldValue, newValue) => await layer.FilePropertyChanged(newValue)); + /// + /// May be overridden to create a derived MBTileSource that handles other tile formats than png and jpg. + /// + protected virtual async Task CreateTileSourceAsync(string file) + { + var tileSource = new MBTileSource(); - public string File + await tileSource.OpenAsync(file); + + if (tileSource.Metadata.TryGetValue("format", out string format) && format != "png" && format != "jpg") { - get => (string)GetValue(FileProperty); - set => SetValue(FileProperty, value); + tileSource.Dispose(); + + throw new NotSupportedException($"Tile image format {format} is not supported."); } - /// - /// May be overridden to create a derived MBTileSource that handles other tile formats than png and jpg. - /// - protected virtual async Task CreateTileSourceAsync(string file) + return tileSource; + } + + private async Task FilePropertyChanged(string file) + { + (TileSource as MBTileSource)?.Close(); + + ClearValue(TileSourceProperty); + ClearValue(SourceNameProperty); + ClearValue(DescriptionProperty); + ClearValue(MinZoomLevelProperty); + ClearValue(MaxZoomLevelProperty); + + if (!string.IsNullOrEmpty(file)) { - var tileSource = new MBTileSource(); - - await tileSource.OpenAsync(file); - - if (tileSource.Metadata.TryGetValue("format", out string format) && format != "png" && format != "jpg") + try { - tileSource.Dispose(); + var tileSource = await CreateTileSourceAsync(file); - throw new NotSupportedException($"Tile image format {format} is not supported."); + TileSource = tileSource; + + if (tileSource.Metadata.TryGetValue("name", out string value)) + { + SourceName = value; + } + + if (tileSource.Metadata.TryGetValue("description", out value)) + { + Description = value; + } + + if (tileSource.Metadata.TryGetValue("minzoom", out value) && int.TryParse(value, out int zoomLevel)) + { + MinZoomLevel = zoomLevel; + } + + if (tileSource.Metadata.TryGetValue("maxzoom", out value) && int.TryParse(value, out zoomLevel)) + { + MaxZoomLevel = zoomLevel; + } } - - return tileSource; - } - - private async Task FilePropertyChanged(string file) - { - (TileSource as MBTileSource)?.Close(); - - ClearValue(TileSourceProperty); - ClearValue(SourceNameProperty); - ClearValue(DescriptionProperty); - ClearValue(MinZoomLevelProperty); - ClearValue(MaxZoomLevelProperty); - - if (!string.IsNullOrEmpty(file)) + catch (Exception ex) { - try - { - var tileSource = await CreateTileSourceAsync(file); - - TileSource = tileSource; - - if (tileSource.Metadata.TryGetValue("name", out string value)) - { - SourceName = value; - } - - if (tileSource.Metadata.TryGetValue("description", out value)) - { - Description = value; - } - - if (tileSource.Metadata.TryGetValue("minzoom", out value) && int.TryParse(value, out int zoomLevel)) - { - MinZoomLevel = zoomLevel; - } - - if (tileSource.Metadata.TryGetValue("maxzoom", out value) && int.TryParse(value, out zoomLevel)) - { - MaxZoomLevel = zoomLevel; - } - } - catch (Exception ex) - { - Logger?.LogError(ex, "Invalid file: {file}", file); - } + Logger?.LogError(ex, "Invalid file: {file}", file); } } } diff --git a/MBTiles/Shared/MBTileSource.cs b/MBTiles/Shared/MBTileSource.cs index a58f4192..97ed3510 100644 --- a/MBTiles/Shared/MBTileSource.cs +++ b/MBTiles/Shared/MBTileSource.cs @@ -13,74 +13,73 @@ using Microsoft.UI.Xaml.Media; using ImageSource = Avalonia.Media.IImage; #endif -namespace MapControl.MBTiles +namespace MapControl.MBTiles; + +public sealed partial class MBTileSource : TileSource, IDisposable { - public sealed partial class MBTileSource : TileSource, IDisposable + private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(); + + private SQLiteConnection connection; + + public IDictionary Metadata { get; } = new Dictionary(); + + public async Task OpenAsync(string file) { - private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(); + Close(); - private SQLiteConnection connection; + connection = new SQLiteConnection("Data Source=" + FilePath.GetFullPath(file) + ";Read Only=True"); - public IDictionary Metadata { get; } = new Dictionary(); + await connection.OpenAsync(); - public async Task OpenAsync(string file) + using var command = new SQLiteCommand("select * from metadata", connection); + + var reader = await command.ExecuteReaderAsync(); + + while (await reader.ReadAsync()) { - Close(); - - connection = new SQLiteConnection("Data Source=" + FilePath.GetFullPath(file) + ";Read Only=True"); - - await connection.OpenAsync(); - - using var command = new SQLiteCommand("select * from metadata", connection); - - var reader = await command.ExecuteReaderAsync(); - - while (await reader.ReadAsync()) - { - Metadata[(string)reader["name"]] = (string)reader["value"]; - } - } - - public void Close() - { - if (connection != null) - { - Metadata.Clear(); - connection.Dispose(); - connection = null; - } - } - - public void Dispose() - { - Close(); - } - - public override async Task LoadImageAsync(int zoomLevel, int column, int row) - { - ImageSource image = null; - - try - { - using var command = new SQLiteCommand("select tile_data from tiles where zoom_level=@z and tile_column=@x and tile_row=@y", connection); - - command.Parameters.AddWithValue("@z", zoomLevel); - command.Parameters.AddWithValue("@x", column); - command.Parameters.AddWithValue("@y", (1 << zoomLevel) - row - 1); - - var buffer = (byte[])await command.ExecuteScalarAsync(); - - if (buffer?.Length > 0) - { - image = await ImageLoader.LoadImageAsync(buffer); - } - } - catch (Exception ex) - { - Logger?.LogError(ex, "LoadImageAsync"); - } - - return image; + Metadata[(string)reader["name"]] = (string)reader["value"]; } } + + public void Close() + { + if (connection != null) + { + Metadata.Clear(); + connection.Dispose(); + connection = null; + } + } + + public void Dispose() + { + Close(); + } + + public override async Task LoadImageAsync(int zoomLevel, int column, int row) + { + ImageSource image = null; + + try + { + using var command = new SQLiteCommand("select tile_data from tiles where zoom_level=@z and tile_column=@x and tile_row=@y", connection); + + command.Parameters.AddWithValue("@z", zoomLevel); + command.Parameters.AddWithValue("@x", column); + command.Parameters.AddWithValue("@y", (1 << zoomLevel) - row - 1); + + var buffer = (byte[])await command.ExecuteScalarAsync(); + + if (buffer?.Length > 0) + { + image = await ImageLoader.LoadImageAsync(buffer); + } + } + catch (Exception ex) + { + Logger?.LogError(ex, "LoadImageAsync"); + } + + return image; + } } diff --git a/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs b/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs index 57d2adc0..5b7f5238 100644 --- a/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs +++ b/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs @@ -6,98 +6,97 @@ using System; #pragma warning disable AVP1001 -namespace MapControl +namespace MapControl; + +public static class DependencyPropertyHelper { - public static class DependencyPropertyHelper + public static AttachedProperty RegisterAttached( + string name, + Type ownerType, + TValue defaultValue = default, + Action changed = null, + bool inherits = false) { - public static AttachedProperty RegisterAttached( - string name, - Type ownerType, - TValue defaultValue = default, - Action changed = null, - bool inherits = false) + var property = AvaloniaProperty.RegisterAttached(name, ownerType, defaultValue, inherits); + + if (changed != null) { - var property = AvaloniaProperty.RegisterAttached(name, ownerType, defaultValue, inherits); - - if (changed != null) - { - property.Changed.AddClassHandler((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value)); - } - - return property; + property.Changed.AddClassHandler((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value)); } - public static StyledProperty Register( - string name, - TValue defaultValue = default, - Action changed = null, - Func coerce = null, - bool bindTwoWayByDefault = false) - where TOwner : AvaloniaObject + return property; + } + + public static StyledProperty Register( + string name, + TValue defaultValue = default, + Action changed = null, + Func coerce = null, + bool bindTwoWayByDefault = false) + where TOwner : AvaloniaObject + { + Func coerceFunc = null; + + if (coerce != null) { - Func coerceFunc = null; - - if (coerce != null) - { - // Do not coerce default value. - // - coerceFunc = (obj, value) => Equals(value, defaultValue) ? value : coerce((TOwner)obj, value); - } - - var bindingMode = bindTwoWayByDefault ? BindingMode.TwoWay : BindingMode.OneWay; - - var property = AvaloniaProperty.Register(name, defaultValue, false, bindingMode, null, coerceFunc); - - if (changed != null) - { - property.Changed.AddClassHandler((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value)); - } - - return property; + // Do not coerce default value. + // + coerceFunc = (obj, value) => Equals(value, defaultValue) ? value : coerce((TOwner)obj, value); } - public static StyledProperty AddOwner( - StyledProperty source) where TOwner : AvaloniaObject - { - return source.AddOwner(); - } + var bindingMode = bindTwoWayByDefault ? BindingMode.TwoWay : BindingMode.OneWay; - public static StyledProperty AddOwner( - StyledProperty source, - TValue defaultValue) where TOwner : AvaloniaObject - { - return source.AddOwner(new StyledPropertyMetadata(new Optional(defaultValue))); - } + var property = AvaloniaProperty.Register(name, defaultValue, false, bindingMode, null, coerceFunc); - public static StyledProperty AddOwner( - StyledProperty source, - Action changed) where TOwner : AvaloniaObject + if (changed != null) { - var property = source.AddOwner(); - property.Changed.AddClassHandler((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value)); - - return property; } - public static StyledProperty AddOwner( - string _, // for compatibility with WinUI/UWP DependencyPropertyHelper - StyledProperty source) where TOwner : AvaloniaObject - { - return AddOwner(source); - } + return property; + } - public static StyledProperty AddOwner( - string _, // for compatibility with WinUI/UWP DependencyPropertyHelper - StyledProperty source, - Action changed) where TOwner : AvaloniaObject - { - return AddOwner(source, changed); - } + public static StyledProperty AddOwner( + StyledProperty source) where TOwner : AvaloniaObject + { + return source.AddOwner(); + } - public static void SetBinding(this AvaloniaObject target, AvaloniaProperty property, Binding binding) - { - target.Bind(property, binding); - } + public static StyledProperty AddOwner( + StyledProperty source, + TValue defaultValue) where TOwner : AvaloniaObject + { + return source.AddOwner(new StyledPropertyMetadata(new Optional(defaultValue))); + } + + public static StyledProperty AddOwner( + StyledProperty source, + Action changed) where TOwner : AvaloniaObject + { + var property = source.AddOwner(); + + property.Changed.AddClassHandler((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value)); + + return property; + } + + public static StyledProperty AddOwner( + string _, // for compatibility with WinUI/UWP DependencyPropertyHelper + StyledProperty source) where TOwner : AvaloniaObject + { + return AddOwner(source); + } + + public static StyledProperty AddOwner( + string _, // for compatibility with WinUI/UWP DependencyPropertyHelper + StyledProperty source, + Action changed) where TOwner : AvaloniaObject + { + return AddOwner(source, changed); + } + + public static void SetBinding(this AvaloniaObject target, AvaloniaProperty property, Binding binding) + { + target.Bind(property, binding); } } diff --git a/MapControl/Avalonia/GeoImage.Avalonia.cs b/MapControl/Avalonia/GeoImage.Avalonia.cs index 9ca061a0..508f64b7 100644 --- a/MapControl/Avalonia/GeoImage.Avalonia.cs +++ b/MapControl/Avalonia/GeoImage.Avalonia.cs @@ -1,13 +1,12 @@ using System; using System.Threading.Tasks; -namespace MapControl +namespace MapControl; + +public static partial class GeoImage { - public static partial class GeoImage + private static Task LoadGeoTiff(string sourcePath) { - private static Task LoadGeoTiff(string sourcePath) - { - throw new InvalidOperationException("GeoTIFF is not supported."); - } + throw new InvalidOperationException("GeoTIFF is not supported."); } } diff --git a/MapControl/Avalonia/ImageLoader.Avalonia.cs b/MapControl/Avalonia/ImageLoader.Avalonia.cs index 34140fe9..c008f9d9 100644 --- a/MapControl/Avalonia/ImageLoader.Avalonia.cs +++ b/MapControl/Avalonia/ImageLoader.Avalonia.cs @@ -7,79 +7,78 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace MapControl +namespace MapControl; + +public static partial class ImageLoader { - public static partial class ImageLoader + public static IImage LoadResourceImage(Uri uri) { - public static IImage LoadResourceImage(Uri uri) + return new Bitmap(AssetLoader.Open(uri)); + } + + public static IImage LoadImage(Stream stream) + { + return new Bitmap(stream); + } + + public static IImage LoadImage(string path) + { + return File.Exists(path) ? new Bitmap(path) : null; + } + + public static Task LoadImageAsync(Stream stream) + { + return Thread.CurrentThread.IsThreadPoolThread ? + Task.FromResult(LoadImage(stream)) : + Task.Run(() => LoadImage(stream)); + } + + public static Task LoadImageAsync(string path) + { + return Thread.CurrentThread.IsThreadPoolThread ? + Task.FromResult(LoadImage(path)) : + Task.Run(() => LoadImage(path)); + } + + internal static async Task LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress progress) + { + Bitmap mergedBitmap = null; + var p1 = 0d; + var p2 = 0d; + + var images = await Task.WhenAll( + LoadImageAsync(uri1, new Progress(p => { p1 = p; progress.Report((p1 + p2) / 2d); })), + LoadImageAsync(uri2, new Progress(p => { p2 = p; progress.Report((p1 + p2) / 2d); }))); + + if (images.Length == 2 && + images[0] is Bitmap bitmap1 && + images[1] is Bitmap bitmap2 && + bitmap1.PixelSize.Height == bitmap2.PixelSize.Height && + bitmap1.Format.HasValue && + bitmap1.Format == bitmap2.Format && + bitmap1.AlphaFormat.HasValue && + bitmap1.AlphaFormat == bitmap2.AlphaFormat) { - return new Bitmap(AssetLoader.Open(uri)); - } + var bpp = bitmap1.Format.Value == PixelFormat.Rgb565 ? 2 : 4; + var pixelSize = new PixelSize(bitmap1.PixelSize.Width + bitmap2.PixelSize.Width, bitmap1.PixelSize.Height); + var stride1 = bpp * bitmap1.PixelSize.Width; + var stride = bpp * pixelSize.Width; + var bufferSize = stride * pixelSize.Height; - public static IImage LoadImage(Stream stream) - { - return new Bitmap(stream); - } - - public static IImage LoadImage(string path) - { - return File.Exists(path) ? new Bitmap(path) : null; - } - - public static Task LoadImageAsync(Stream stream) - { - return Thread.CurrentThread.IsThreadPoolThread ? - Task.FromResult(LoadImage(stream)) : - Task.Run(() => LoadImage(stream)); - } - - public static Task LoadImageAsync(string path) - { - return Thread.CurrentThread.IsThreadPoolThread ? - Task.FromResult(LoadImage(path)) : - Task.Run(() => LoadImage(path)); - } - - internal static async Task LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress progress) - { - Bitmap mergedBitmap = null; - var p1 = 0d; - var p2 = 0d; - - var images = await Task.WhenAll( - LoadImageAsync(uri1, new Progress(p => { p1 = p; progress.Report((p1 + p2) / 2d); })), - LoadImageAsync(uri2, new Progress(p => { p2 = p; progress.Report((p1 + p2) / 2d); }))); - - if (images.Length == 2 && - images[0] is Bitmap bitmap1 && - images[1] is Bitmap bitmap2 && - bitmap1.PixelSize.Height == bitmap2.PixelSize.Height && - bitmap1.Format.HasValue && - bitmap1.Format == bitmap2.Format && - bitmap1.AlphaFormat.HasValue && - bitmap1.AlphaFormat == bitmap2.AlphaFormat) + unsafe { - var bpp = bitmap1.Format.Value == PixelFormat.Rgb565 ? 2 : 4; - var pixelSize = new PixelSize(bitmap1.PixelSize.Width + bitmap2.PixelSize.Width, bitmap1.PixelSize.Height); - var stride1 = bpp * bitmap1.PixelSize.Width; - var stride = bpp * pixelSize.Width; - var bufferSize = stride * pixelSize.Height; - - unsafe + fixed (byte* ptr = new byte[stride * pixelSize.Height]) { - fixed (byte* ptr = new byte[stride * pixelSize.Height]) - { - var buffer = (nint)ptr; + var buffer = (nint)ptr; - bitmap1.CopyPixels(new PixelRect(bitmap1.PixelSize), buffer, bufferSize, stride); - bitmap2.CopyPixels(new PixelRect(bitmap2.PixelSize), buffer + stride1, bufferSize, stride); + bitmap1.CopyPixels(new PixelRect(bitmap1.PixelSize), buffer, bufferSize, stride); + bitmap2.CopyPixels(new PixelRect(bitmap2.PixelSize), buffer + stride1, bufferSize, stride); - mergedBitmap = new Bitmap(bitmap1.Format.Value, bitmap1.AlphaFormat.Value, buffer, pixelSize, bitmap1.Dpi, stride); - } + mergedBitmap = new Bitmap(bitmap1.Format.Value, bitmap1.AlphaFormat.Value, buffer, pixelSize, bitmap1.Dpi, stride); } } - - return mergedBitmap; } + + return mergedBitmap; } } diff --git a/MapControl/Avalonia/ImageTile.Avalonia.cs b/MapControl/Avalonia/ImageTile.Avalonia.cs index 87086d35..d2e99245 100644 --- a/MapControl/Avalonia/ImageTile.Avalonia.cs +++ b/MapControl/Avalonia/ImageTile.Avalonia.cs @@ -6,46 +6,45 @@ using Avalonia.Styling; using System; using System.Threading.Tasks; -namespace MapControl +namespace MapControl; + +public class ImageTile(int zoomLevel, int x, int y, int columnCount) + : Tile(zoomLevel, x, y, columnCount) { - public class ImageTile(int zoomLevel, int x, int y, int columnCount) - : Tile(zoomLevel, x, y, columnCount) + public Image Image { get; } = new Image { Stretch = Stretch.Fill }; + + public override async Task LoadImageAsync(Func> loadImageFunc) { - public Image Image { get; } = new Image { Stretch = Stretch.Fill }; + var image = await loadImageFunc().ConfigureAwait(false); - public override async Task LoadImageAsync(Func> loadImageFunc) + void SetImageSource() { - var image = await loadImageFunc().ConfigureAwait(false); + Image.Source = image; - void SetImageSource() + if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero) { - Image.Source = image; - - if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero) + var fadeInAnimation = new Animation { - var fadeInAnimation = new Animation - { - Duration = MapBase.ImageFadeDuration, - Children = + Duration = MapBase.ImageFadeDuration, + Children = + { + new KeyFrame { - new KeyFrame - { - KeyTime = TimeSpan.Zero, - Setters = { new Setter(Visual.OpacityProperty, 0d) } - }, - new KeyFrame - { - KeyTime = MapBase.ImageFadeDuration, - Setters = { new Setter(Visual.OpacityProperty, 1d) } - } + KeyTime = TimeSpan.Zero, + Setters = { new Setter(Visual.OpacityProperty, 0d) } + }, + new KeyFrame + { + KeyTime = MapBase.ImageFadeDuration, + Setters = { new Setter(Visual.OpacityProperty, 1d) } } - }; + } + }; - _ = fadeInAnimation.RunAsync(Image); - } + _ = fadeInAnimation.RunAsync(Image); } - - await Image.Dispatcher.InvokeAsync(SetImageSource); } + + await Image.Dispatcher.InvokeAsync(SetImageSource); } } diff --git a/MapControl/Avalonia/LocationAnimator.Avalonia.cs b/MapControl/Avalonia/LocationAnimator.Avalonia.cs index fa383681..8c5f7790 100644 --- a/MapControl/Avalonia/LocationAnimator.Avalonia.cs +++ b/MapControl/Avalonia/LocationAnimator.Avalonia.cs @@ -1,14 +1,13 @@ using Avalonia.Animation; -namespace MapControl +namespace MapControl; + +public class LocationAnimator : InterpolatingAnimator { - public class LocationAnimator : InterpolatingAnimator + public override Location Interpolate(double progress, Location oldValue, Location newValue) { - public override Location Interpolate(double progress, Location oldValue, Location newValue) - { - return new Location( - (1d - progress) * oldValue.Latitude + progress * newValue.Latitude, - (1d - progress) * oldValue.Longitude + progress * newValue.Longitude); - } + return new Location( + (1d - progress) * oldValue.Latitude + progress * newValue.Latitude, + (1d - progress) * oldValue.Longitude + progress * newValue.Longitude); } } diff --git a/MapControl/Avalonia/Map.Avalonia.cs b/MapControl/Avalonia/Map.Avalonia.cs index 5baabd97..4963e868 100644 --- a/MapControl/Avalonia/Map.Avalonia.cs +++ b/MapControl/Avalonia/Map.Avalonia.cs @@ -2,140 +2,139 @@ using Avalonia.Input; using System; -namespace MapControl +namespace MapControl; + +[Flags] +public enum ManipulationModes { - [Flags] - public enum ManipulationModes + None = 0, + Translate = 1, + Rotate = 2, + Scale = 4, + All = Translate | Rotate | Scale +} + +public partial class Map +{ + public static readonly StyledProperty ManipulationModeProperty = + DependencyPropertyHelper.Register(nameof(ManipulationMode), ManipulationModes.Translate | ManipulationModes.Scale); + + /// + /// Gets or sets a value that specifies how the map control handles manipulations. + /// + public ManipulationModes ManipulationMode { - None = 0, - Translate = 1, - Rotate = 2, - Scale = 4, - All = Translate | Rotate | Scale + get => GetValue(ManipulationModeProperty); + set => SetValue(ManipulationModeProperty, value); } - public partial class Map + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { - public static readonly StyledProperty ManipulationModeProperty = - DependencyPropertyHelper.Register(nameof(ManipulationMode), ManipulationModes.Translate | ManipulationModes.Scale); + base.OnPointerWheelChanged(e); - /// - /// Gets or sets a value that specifies how the map control handles manipulations. - /// - public ManipulationModes ManipulationMode + OnMouseWheel(e.GetPosition(this), e.Delta.Y); + } + + private IPointer pointer1; + private IPointer pointer2; + private Point position1; + private Point position2; + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + + var point = e.GetCurrentPoint(this); + + if (point.Pointer == pointer1 || point.Pointer == pointer2) { - get => GetValue(ManipulationModeProperty); - set => SetValue(ManipulationModeProperty, value); - } - - protected override void OnPointerWheelChanged(PointerWheelEventArgs e) - { - base.OnPointerWheelChanged(e); - - OnMouseWheel(e.GetPosition(this), e.Delta.Y); - } - - private IPointer pointer1; - private IPointer pointer2; - private Point position1; - private Point position2; - - protected override void OnPointerMoved(PointerEventArgs e) - { - base.OnPointerMoved(e); - - var point = e.GetCurrentPoint(this); - - if (point.Pointer == pointer1 || point.Pointer == pointer2) + if (pointer2 != null) { - if (pointer2 != null) - { - HandleManipulation(point.Pointer, point.Position); - } - else if (point.Pointer.Type == PointerType.Mouse || - ManipulationMode.HasFlag(ManipulationModes.Translate)) - { - TranslateMap(new Point(point.Position.X - position1.X, point.Position.Y - position1.Y)); - position1 = point.Position; - } + HandleManipulation(point.Pointer, point.Position); } - else if (pointer1 == null && - point.Pointer.Type == PointerType.Mouse && - point.Properties.IsLeftButtonPressed && - e.KeyModifiers == KeyModifiers.None || - pointer2 == null && - point.Pointer.Type == PointerType.Touch && - ManipulationMode != ManipulationModes.None) + else if (point.Pointer.Type == PointerType.Mouse || + ManipulationMode.HasFlag(ManipulationModes.Translate)) { - point.Pointer.Capture(this); - - if (pointer1 == null) - { - pointer1 = point.Pointer; - position1 = point.Position; - } - else - { - pointer2 = point.Pointer; - position2 = point.Position; - } + TranslateMap(new Point(point.Position.X - position1.X, point.Position.Y - position1.Y)); + position1 = point.Position; } } - - protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + else if (pointer1 == null && + point.Pointer.Type == PointerType.Mouse && + point.Properties.IsLeftButtonPressed && + e.KeyModifiers == KeyModifiers.None || + pointer2 == null && + point.Pointer.Type == PointerType.Touch && + ManipulationMode != ManipulationModes.None) { - base.OnPointerCaptureLost(e); + point.Pointer.Capture(this); - if (e.Pointer == pointer1 || e.Pointer == pointer2) + if (pointer1 == null) { - if (e.Pointer == pointer1) - { - pointer1 = pointer2; - position1 = position2; - } - - pointer2 = null; - } - } - - private void HandleManipulation(IPointer pointer, Point position) - { - var oldDistance = new Vector(position2.X - position1.X, position2.Y - position1.Y); - var oldOrigin = new Point((position1.X + position2.X) / 2d, (position1.Y + position2.Y) / 2d); - - if (pointer == pointer1) - { - position1 = position; + pointer1 = point.Pointer; + position1 = point.Position; } else { - position2 = position; + pointer2 = point.Pointer; + position2 = point.Position; } - - var newDistance = new Vector(position2.X - position1.X, position2.Y - position1.Y); - var newOrigin = oldOrigin; - var translation = new Point(); - var rotation = 0d; - var scale = 1d; - - if (ManipulationMode.HasFlag(ManipulationModes.Translate)) - { - newOrigin = new Point((position1.X + position2.X) / 2d, (position1.Y + position2.Y) / 2d); - translation = newOrigin - oldOrigin; - } - - if (ManipulationMode.HasFlag(ManipulationModes.Rotate)) - { - rotation = 180d / Math.PI * - (Math.Atan2(newDistance.Y, newDistance.X) - Math.Atan2(oldDistance.Y, oldDistance.X)); - } - - if (ManipulationMode.HasFlag(ManipulationModes.Scale)) - { - scale = newDistance.Length / oldDistance.Length; - } - - TransformMap(newOrigin, translation, rotation, scale); } } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + base.OnPointerCaptureLost(e); + + if (e.Pointer == pointer1 || e.Pointer == pointer2) + { + if (e.Pointer == pointer1) + { + pointer1 = pointer2; + position1 = position2; + } + + pointer2 = null; + } + } + + private void HandleManipulation(IPointer pointer, Point position) + { + var oldDistance = new Vector(position2.X - position1.X, position2.Y - position1.Y); + var oldOrigin = new Point((position1.X + position2.X) / 2d, (position1.Y + position2.Y) / 2d); + + if (pointer == pointer1) + { + position1 = position; + } + else + { + position2 = position; + } + + var newDistance = new Vector(position2.X - position1.X, position2.Y - position1.Y); + var newOrigin = oldOrigin; + var translation = new Point(); + var rotation = 0d; + var scale = 1d; + + if (ManipulationMode.HasFlag(ManipulationModes.Translate)) + { + newOrigin = new Point((position1.X + position2.X) / 2d, (position1.Y + position2.Y) / 2d); + translation = newOrigin - oldOrigin; + } + + if (ManipulationMode.HasFlag(ManipulationModes.Rotate)) + { + rotation = 180d / Math.PI * + (Math.Atan2(newDistance.Y, newDistance.X) - Math.Atan2(oldDistance.Y, oldDistance.X)); + } + + if (ManipulationMode.HasFlag(ManipulationModes.Scale)) + { + scale = newDistance.Length / oldDistance.Length; + } + + TransformMap(newOrigin, translation, rotation, scale); + } } diff --git a/MapControl/Avalonia/MapBase.Avalonia.cs b/MapControl/Avalonia/MapBase.Avalonia.cs index 6a0534a2..f53ba92b 100644 --- a/MapControl/Avalonia/MapBase.Avalonia.cs +++ b/MapControl/Avalonia/MapBase.Avalonia.cs @@ -10,266 +10,265 @@ using System.Threading; using System.Threading.Tasks; using Brush = Avalonia.Media.IBrush; -namespace MapControl +namespace MapControl; + +public partial class MapBase { - public partial class MapBase + public static readonly StyledProperty ForegroundProperty = + DependencyPropertyHelper.AddOwner(TextElement.ForegroundProperty, Brushes.Black); + + public static readonly StyledProperty AnimationEasingProperty = + DependencyPropertyHelper.Register(nameof(AnimationEasing), new QuadraticEaseOut()); + + public static readonly StyledProperty CenterProperty = + DependencyPropertyHelper.Register(nameof(Center), new Location(0d, 0d), + (map, oldValue, newValue) => map.CenterPropertyChanged(newValue), + (map, value) => map.CoerceCenterProperty(value), + true); + + public static readonly StyledProperty TargetCenterProperty = + DependencyPropertyHelper.Register(nameof(TargetCenter), new Location(0d, 0d), + async (map, oldValue, newValue) => await map.TargetCenterPropertyChanged(newValue), + (map, value) => map.CoerceCenterProperty(value), + true); + + public static readonly StyledProperty MinZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(MinZoomLevel), 1d, + (map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceMinZoomLevelProperty(value)); + + public static readonly StyledProperty MaxZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(MaxZoomLevel), 20d, + (map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceMaxZoomLevelProperty(value)); + + public static readonly StyledProperty ZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(ZoomLevel), 1d, + (map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceZoomLevelProperty(value), + true); + + public static readonly StyledProperty TargetZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(TargetZoomLevel), 1d, + async (map, oldValue, newValue) => await map.TargetZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceZoomLevelProperty(value), + true); + + public static readonly StyledProperty HeadingProperty = + DependencyPropertyHelper.Register(nameof(Heading), 0d, + (map, oldValue, newValue) => map.HeadingPropertyChanged(newValue), + (map, value) => map.CoerceHeadingProperty(value), + true); + + public static readonly StyledProperty TargetHeadingProperty = + DependencyPropertyHelper.Register(nameof(TargetHeading), 0d, + async (map, oldValue, newValue) => await map.TargetHeadingPropertyChanged(newValue), + (map, value) => map.CoerceHeadingProperty(value), + true); + + public static readonly DirectProperty ViewScaleProperty = + AvaloniaProperty.RegisterDirect(nameof(ViewScale), map => map.ViewTransform.Scale); + + private CancellationTokenSource centerCts; + private CancellationTokenSource zoomLevelCts; + private CancellationTokenSource headingCts; + private Animation centerAnimation; + private Animation zoomLevelAnimation; + private Animation headingAnimation; + + static MapBase() { - public static readonly StyledProperty ForegroundProperty = - DependencyPropertyHelper.AddOwner(TextElement.ForegroundProperty, Brushes.Black); + BackgroundProperty.OverrideDefaultValue(Brushes.White); + ClipToBoundsProperty.OverrideDefaultValue(true); - public static readonly StyledProperty AnimationEasingProperty = - DependencyPropertyHelper.Register(nameof(AnimationEasing), new QuadraticEaseOut()); + Animation.RegisterCustomAnimator(); + } - public static readonly StyledProperty CenterProperty = - DependencyPropertyHelper.Register(nameof(Center), new Location(0d, 0d), - (map, oldValue, newValue) => map.CenterPropertyChanged(newValue), - (map, value) => map.CoerceCenterProperty(value), - true); + public double ActualWidth => Bounds.Width; + public double ActualHeight => Bounds.Height; - public static readonly StyledProperty TargetCenterProperty = - DependencyPropertyHelper.Register(nameof(TargetCenter), new Location(0d, 0d), - async (map, oldValue, newValue) => await map.TargetCenterPropertyChanged(newValue), - (map, value) => map.CoerceCenterProperty(value), - true); + protected override void OnSizeChanged(SizeChangedEventArgs e) + { + base.OnSizeChanged(e); - public static readonly StyledProperty MinZoomLevelProperty = - DependencyPropertyHelper.Register(nameof(MinZoomLevel), 1d, - (map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue), - (map, value) => map.CoerceMinZoomLevelProperty(value)); + ResetTransformCenter(); + UpdateTransform(); + } - public static readonly StyledProperty MaxZoomLevelProperty = - DependencyPropertyHelper.Register(nameof(MaxZoomLevel), 20d, - (map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue), - (map, value) => map.CoerceMaxZoomLevelProperty(value)); + /// + /// Gets or sets the Easing of the Center, ZoomLevel and Heading animations. + /// The default value is a QuadraticEaseOut. + /// + public Easing AnimationEasing + { + get => GetValue(AnimationEasingProperty); + set => SetValue(AnimationEasingProperty, value); + } - public static readonly StyledProperty ZoomLevelProperty = - DependencyPropertyHelper.Register(nameof(ZoomLevel), 1d, - (map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue), - (map, value) => map.CoerceZoomLevelProperty(value), - true); + /// + /// Gets the scaling factor from projected map coordinates to view coordinates, + /// as pixels per meter. + /// + public double ViewScale + { + get => GetValue(ViewScaleProperty); + private set => RaisePropertyChanged(ViewScaleProperty, double.NaN, value); + } - public static readonly StyledProperty TargetZoomLevelProperty = - DependencyPropertyHelper.Register(nameof(TargetZoomLevel), 1d, - async (map, oldValue, newValue) => await map.TargetZoomLevelPropertyChanged(newValue), - (map, value) => map.CoerceZoomLevelProperty(value), - true); - - public static readonly StyledProperty HeadingProperty = - DependencyPropertyHelper.Register(nameof(Heading), 0d, - (map, oldValue, newValue) => map.HeadingPropertyChanged(newValue), - (map, value) => map.CoerceHeadingProperty(value), - true); - - public static readonly StyledProperty TargetHeadingProperty = - DependencyPropertyHelper.Register(nameof(TargetHeading), 0d, - async (map, oldValue, newValue) => await map.TargetHeadingPropertyChanged(newValue), - (map, value) => map.CoerceHeadingProperty(value), - true); - - public static readonly DirectProperty ViewScaleProperty = - AvaloniaProperty.RegisterDirect(nameof(ViewScale), map => map.ViewTransform.Scale); - - private CancellationTokenSource centerCts; - private CancellationTokenSource zoomLevelCts; - private CancellationTokenSource headingCts; - private Animation centerAnimation; - private Animation zoomLevelAnimation; - private Animation headingAnimation; - - static MapBase() + private void CenterPropertyChanged(Location center) + { + if (!internalPropertyChange) { - BackgroundProperty.OverrideDefaultValue(Brushes.White); - ClipToBoundsProperty.OverrideDefaultValue(true); - - Animation.RegisterCustomAnimator(); - } - - public double ActualWidth => Bounds.Width; - public double ActualHeight => Bounds.Height; - - protected override void OnSizeChanged(SizeChangedEventArgs e) - { - base.OnSizeChanged(e); - - ResetTransformCenter(); UpdateTransform(); - } - /// - /// Gets or sets the Easing of the Center, ZoomLevel and Heading animations. - /// The default value is a QuadraticEaseOut. - /// - public Easing AnimationEasing - { - get => GetValue(AnimationEasingProperty); - set => SetValue(AnimationEasingProperty, value); - } - - /// - /// Gets the scaling factor from projected map coordinates to view coordinates, - /// as pixels per meter. - /// - public double ViewScale - { - get => GetValue(ViewScaleProperty); - private set => RaisePropertyChanged(ViewScaleProperty, double.NaN, value); - } - - private void CenterPropertyChanged(Location center) - { - if (!internalPropertyChange) + if (centerAnimation == null) { - UpdateTransform(); - - if (centerAnimation == null) - { - SetValueInternal(TargetCenterProperty, center); - } + SetValueInternal(TargetCenterProperty, center); } } - - private async Task TargetCenterPropertyChanged(Location targetCenter) - { - if (!internalPropertyChange && !targetCenter.Equals(Center)) - { - ResetTransformCenter(); - - centerCts?.Cancel(); - - centerAnimation = CreateAnimation(CenterProperty, new Location(targetCenter.Latitude, NearestLongitude(targetCenter.Longitude))); - - using (centerCts = new CancellationTokenSource()) - { - await centerAnimation.RunAsync(this, centerCts.Token); - - if (!centerCts.IsCancellationRequested) - { - UpdateTransform(); - } - } - - centerCts = null; - centerAnimation = null; - } - } - - private void MinZoomLevelPropertyChanged(double minZoomLevel) - { - if (ZoomLevel < minZoomLevel) - { - ZoomLevel = minZoomLevel; - } - } - - private void MaxZoomLevelPropertyChanged(double maxZoomLevel) - { - if (ZoomLevel > maxZoomLevel) - { - ZoomLevel = maxZoomLevel; - } - } - - private void ZoomLevelPropertyChanged(double zoomLevel) - { - if (!internalPropertyChange) - { - UpdateTransform(); - - if (zoomLevelAnimation == null) - { - SetValueInternal(TargetZoomLevelProperty, zoomLevel); - } - } - } - - private async Task TargetZoomLevelPropertyChanged(double targetZoomLevel) - { - if (!internalPropertyChange && targetZoomLevel != ZoomLevel) - { - zoomLevelCts?.Cancel(); - - zoomLevelAnimation = CreateAnimation(ZoomLevelProperty, targetZoomLevel); - - using (zoomLevelCts = new CancellationTokenSource()) - { - await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token); - - if (!zoomLevelCts.IsCancellationRequested) - { - UpdateTransform(true); // reset transform center - } - } - - zoomLevelCts = null; - zoomLevelAnimation = null; - } - } - - private void HeadingPropertyChanged(double heading) - { - if (!internalPropertyChange) - { - UpdateTransform(); - - if (headingAnimation == null) - { - SetValueInternal(TargetHeadingProperty, heading); - } - } - } - - private async Task TargetHeadingPropertyChanged(double targetHeading) - { - if (!internalPropertyChange && targetHeading != Heading) - { - var delta = targetHeading - Heading; - - if (delta > 180d) - { - delta -= 360d; - } - else if (delta < -180d) - { - delta += 360d; - } - - targetHeading = Heading + delta; - - headingCts?.Cancel(); - - headingAnimation = CreateAnimation(HeadingProperty, targetHeading); - - using (headingCts = new CancellationTokenSource()) - { - await headingAnimation.RunAsync(this, headingCts.Token); - - if (!headingCts.IsCancellationRequested) - { - UpdateTransform(); - } - } - - headingCts = null; - headingAnimation = null; - } - } - - private Animation CreateAnimation(DependencyProperty property, object value) - { - return new Animation - { - FillMode = FillMode.Forward, - Duration = AnimationDuration, - Easing = AnimationEasing, - Children = - { - new KeyFrame - { - KeyTime = AnimationDuration, - Setters = { new Setter(property, value) } - } - } - }; - } + } + + private async Task TargetCenterPropertyChanged(Location targetCenter) + { + if (!internalPropertyChange && !targetCenter.Equals(Center)) + { + ResetTransformCenter(); + + centerCts?.Cancel(); + + centerAnimation = CreateAnimation(CenterProperty, new Location(targetCenter.Latitude, NearestLongitude(targetCenter.Longitude))); + + using (centerCts = new CancellationTokenSource()) + { + await centerAnimation.RunAsync(this, centerCts.Token); + + if (!centerCts.IsCancellationRequested) + { + UpdateTransform(); + } + } + + centerCts = null; + centerAnimation = null; + } + } + + private void MinZoomLevelPropertyChanged(double minZoomLevel) + { + if (ZoomLevel < minZoomLevel) + { + ZoomLevel = minZoomLevel; + } + } + + private void MaxZoomLevelPropertyChanged(double maxZoomLevel) + { + if (ZoomLevel > maxZoomLevel) + { + ZoomLevel = maxZoomLevel; + } + } + + private void ZoomLevelPropertyChanged(double zoomLevel) + { + if (!internalPropertyChange) + { + UpdateTransform(); + + if (zoomLevelAnimation == null) + { + SetValueInternal(TargetZoomLevelProperty, zoomLevel); + } + } + } + + private async Task TargetZoomLevelPropertyChanged(double targetZoomLevel) + { + if (!internalPropertyChange && targetZoomLevel != ZoomLevel) + { + zoomLevelCts?.Cancel(); + + zoomLevelAnimation = CreateAnimation(ZoomLevelProperty, targetZoomLevel); + + using (zoomLevelCts = new CancellationTokenSource()) + { + await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token); + + if (!zoomLevelCts.IsCancellationRequested) + { + UpdateTransform(true); // reset transform center + } + } + + zoomLevelCts = null; + zoomLevelAnimation = null; + } + } + + private void HeadingPropertyChanged(double heading) + { + if (!internalPropertyChange) + { + UpdateTransform(); + + if (headingAnimation == null) + { + SetValueInternal(TargetHeadingProperty, heading); + } + } + } + + private async Task TargetHeadingPropertyChanged(double targetHeading) + { + if (!internalPropertyChange && targetHeading != Heading) + { + var delta = targetHeading - Heading; + + if (delta > 180d) + { + delta -= 360d; + } + else if (delta < -180d) + { + delta += 360d; + } + + targetHeading = Heading + delta; + + headingCts?.Cancel(); + + headingAnimation = CreateAnimation(HeadingProperty, targetHeading); + + using (headingCts = new CancellationTokenSource()) + { + await headingAnimation.RunAsync(this, headingCts.Token); + + if (!headingCts.IsCancellationRequested) + { + UpdateTransform(); + } + } + + headingCts = null; + headingAnimation = null; + } + } + + private Animation CreateAnimation(DependencyProperty property, object value) + { + return new Animation + { + FillMode = FillMode.Forward, + Duration = AnimationDuration, + Easing = AnimationEasing, + Children = + { + new KeyFrame + { + KeyTime = AnimationDuration, + Setters = { new Setter(property, value) } + } + } + }; } } diff --git a/MapControl/Avalonia/MapContentControl.Avalonia.cs b/MapControl/Avalonia/MapContentControl.Avalonia.cs deleted file mode 100644 index f8499236..00000000 --- a/MapControl/Avalonia/MapContentControl.Avalonia.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Avalonia; -using Avalonia.Controls; - -namespace MapControl -{ -} diff --git a/MapControl/Avalonia/MapGrid.Avalonia.cs b/MapControl/Avalonia/MapGrid.Avalonia.cs index 31cc0c1e..1ec648b4 100644 --- a/MapControl/Avalonia/MapGrid.Avalonia.cs +++ b/MapControl/Avalonia/MapGrid.Avalonia.cs @@ -7,116 +7,115 @@ using System; using System.Collections.Generic; using System.Globalization; -namespace MapControl +namespace MapControl; + +public partial class MapGrid : Control, IMapElement { - public partial class MapGrid : Control, IMapElement + static MapGrid() { - static MapGrid() + AffectsRender(ForegroundProperty); + } + + public static readonly StyledProperty ForegroundProperty = + DependencyPropertyHelper.AddOwner(TextElement.ForegroundProperty); + + public static readonly StyledProperty FontFamilyProperty = + DependencyPropertyHelper.AddOwner(TextElement.FontFamilyProperty); + + public static readonly StyledProperty FontSizeProperty = + DependencyPropertyHelper.AddOwner(TextElement.FontSizeProperty, 12d); + + /// + /// Implements IMapElement.ParentMap. + /// + public MapBase ParentMap + { + get; + set { - AffectsRender(ForegroundProperty); - } - - public static readonly StyledProperty ForegroundProperty = - DependencyPropertyHelper.AddOwner(TextElement.ForegroundProperty); - - public static readonly StyledProperty FontFamilyProperty = - DependencyPropertyHelper.AddOwner(TextElement.FontFamilyProperty); - - public static readonly StyledProperty FontSizeProperty = - DependencyPropertyHelper.AddOwner(TextElement.FontSizeProperty, 12d); - - /// - /// Implements IMapElement.ParentMap. - /// - public MapBase ParentMap - { - get; - set + if (field != null) { - if (field != null) - { - field.ViewportChanged -= OnViewportChanged; - } + field.ViewportChanged -= OnViewportChanged; + } - field = value; + field = value; - if (field != null) - { - field.ViewportChanged += OnViewportChanged; - } + if (field != null) + { + field.ViewportChanged += OnViewportChanged; } } + } - private void OnViewportChanged(object sender, ViewportChangedEventArgs e) - { - OnViewportChanged(e); - } + private void OnViewportChanged(object sender, ViewportChangedEventArgs e) + { + OnViewportChanged(e); + } - protected virtual void OnViewportChanged(ViewportChangedEventArgs e) - { - InvalidateVisual(); - } + protected virtual void OnViewportChanged(ViewportChangedEventArgs e) + { + InvalidateVisual(); + } - public override void Render(DrawingContext drawingContext) + public override void Render(DrawingContext drawingContext) + { + if (ParentMap != null) { - if (ParentMap != null) + var pathGeometry = new PathGeometry(); + var labels = new List