mirror of
https://github.com/ClemensFischer/XAML-Map-Control.git
synced 2026-04-18 12:55:14 +00:00
File scoped namespaces
This commit is contained in:
parent
c14377f976
commit
65aba44af6
152 changed files with 11962 additions and 12115 deletions
|
|
@ -7,200 +7,199 @@ using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MapControl.Caching
|
namespace MapControl.Caching;
|
||||||
|
|
||||||
|
public class FileDbCacheOptions : IOptions<FileDbCacheOptions>
|
||||||
{
|
{
|
||||||
public class FileDbCacheOptions : IOptions<FileDbCacheOptions>
|
public FileDbCacheOptions Value => this;
|
||||||
|
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromHours(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IDistributedCache implementation based on FileDb, https://github.com/eztools-software/FileDb.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public FileDbCache(IOptions<FileDbCacheOptions> optionsAccessor, ILoggerFactory loggerFactory = null)
|
||||||
/// IDistributedCache implementation based on FileDb, https://github.com/eztools-software/FileDb.
|
: this(optionsAccessor.Value, loggerFactory)
|
||||||
/// </summary>
|
|
||||||
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 };
|
public FileDbCache(FileDbCacheOptions options, ILoggerFactory loggerFactory = null)
|
||||||
private readonly Timer timer;
|
{
|
||||||
private readonly ILogger logger;
|
var path = options.Path;
|
||||||
|
|
||||||
public FileDbCache(string path, ILoggerFactory loggerFactory = null)
|
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(Path.GetExtension(path)))
|
||||||
: this(new FileDbCacheOptions { Path = path }, loggerFactory)
|
|
||||||
{
|
{
|
||||||
|
path = Path.Combine(path ?? "", "TileCache.fdb");
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileDbCache(IOptions<FileDbCacheOptions> optionsAccessor, ILoggerFactory loggerFactory = null)
|
logger = loggerFactory?.CreateLogger<FileDbCache>();
|
||||||
: this(optionsAccessor.Value, loggerFactory)
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
|
fileDb.Open(path);
|
||||||
|
|
||||||
|
logger?.LogInformation("Opened database {path}", path);
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
public FileDbCache(FileDbCacheOptions options, ILoggerFactory loggerFactory = null)
|
|
||||||
{
|
{
|
||||||
var path = options.Path;
|
if (File.Exists(path))
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(Path.GetExtension(path)))
|
|
||||||
{
|
{
|
||||||
path = Path.Combine(path ?? "", "TileCache.fdb");
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
logger = loggerFactory?.CreateLogger<FileDbCache>();
|
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<byte[]> 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
|
try
|
||||||
{
|
{
|
||||||
fileDb.Open(path);
|
if (fileDb.GetRecordByKey(key, new string[0], false) != null)
|
||||||
|
|
||||||
logger?.LogInformation("Opened database {path}", path);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
{
|
||||||
File.Delete(path);
|
fileDb.UpdateRecordByKey(key, fieldValues);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
fieldValues.Add(KeyField, key);
|
||||||
}
|
fileDb.AddRecord(fieldValues);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<byte[]> 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
|
logger?.LogError(ex, "Set({key})", key);
|
||||||
? 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>14.0</LangVersion>
|
||||||
<RootNamespace>MapControl.Caching</RootNamespace>
|
<RootNamespace>MapControl.Caching</RootNamespace>
|
||||||
<AssemblyTitle>XAML Map Control FileDbCache Library</AssemblyTitle>
|
<AssemblyTitle>XAML Map Control FileDbCache Library</AssemblyTitle>
|
||||||
<GeneratePackageOnBuild>$(GeneratePackage)</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>$(GeneratePackage)</GeneratePackageOnBuild>
|
||||||
|
|
|
||||||
|
|
@ -7,250 +7,249 @@ using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MapControl.Caching
|
namespace MapControl.Caching;
|
||||||
|
|
||||||
|
public class SQLiteCacheOptions : IOptions<SQLiteCacheOptions>
|
||||||
{
|
{
|
||||||
public class SQLiteCacheOptions : IOptions<SQLiteCacheOptions>
|
public SQLiteCacheOptions Value => this;
|
||||||
|
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromHours(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IDistributedCache implementation based on System.Data.SQLite, https://system.data.sqlite.org/.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public SQLiteCache(IOptions<SQLiteCacheOptions> optionsAccessor, ILoggerFactory loggerFactory = null)
|
||||||
/// IDistributedCache implementation based on System.Data.SQLite, https://system.data.sqlite.org/.
|
: this(optionsAccessor.Value, loggerFactory)
|
||||||
/// </summary>
|
|
||||||
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)
|
public SQLiteCache(SQLiteCacheOptions options, ILoggerFactory loggerFactory = null)
|
||||||
: this(new SQLiteCacheOptions { Path = path }, loggerFactory)
|
{
|
||||||
|
var path = options.Path;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(Path.GetExtension(path)))
|
||||||
{
|
{
|
||||||
|
path = Path.Combine(path ?? "", "TileCache.sqlite");
|
||||||
}
|
}
|
||||||
|
|
||||||
public SQLiteCache(IOptions<SQLiteCacheOptions> optionsAccessor, ILoggerFactory loggerFactory = null)
|
connection = new SQLiteConnection("Data Source=" + path);
|
||||||
: this(optionsAccessor.Value, loggerFactory)
|
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<SQLiteCache>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
connection = new SQLiteConnection("Data Source=" + path);
|
|
||||||
connection.Open();
|
|
||||||
|
|
||||||
using (var command = new SQLiteCommand("pragma journal_mode=wal", connection))
|
|
||||||
{
|
{
|
||||||
command.ExecuteNonQuery();
|
logger?.LogError(ex, "Get({key})", key);
|
||||||
}
|
|
||||||
|
|
||||||
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<SQLiteCache>();
|
|
||||||
logger?.LogInformation("Opened database {path}", path);
|
|
||||||
|
|
||||||
if (options.ExpirationScanFrequency > TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
timer = new Timer(_ => DeleteExpiredItems(), null, TimeSpan.Zero, options.ExpirationScanFrequency);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
return value;
|
||||||
{
|
}
|
||||||
timer?.Dispose();
|
|
||||||
connection.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] Get(string key)
|
public async Task<byte[]> GetAsync(string key, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
byte[] value = null;
|
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[])await command.ExecuteScalarAsync();
|
||||||
{
|
|
||||||
value = (byte[])command.ExecuteScalar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger?.LogError(ex, "Get({key})", key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<byte[]> GetAsync(string key, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
byte[] value = null;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(key))
|
|
||||||
{
|
{
|
||||||
try
|
logger?.LogError(ex, "GetAsync({key})", key);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
{
|
|
||||||
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();
|
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);
|
try
|
||||||
command.Parameters.AddWithValue("@key", key);
|
{
|
||||||
command.Parameters.AddWithValue("@exp", DateTimeOffset.UtcNow.Ticks);
|
using (var command = SetItemCommand(key, value, options))
|
||||||
return command;
|
{
|
||||||
}
|
await command.ExecuteNonQueryAsync(token);
|
||||||
|
}
|
||||||
private SQLiteCommand SetItemCommand(string key, byte[] buffer, DistributedCacheEntryOptions options)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
var expiration = options.AbsoluteExpiration ??
|
{
|
||||||
DateTimeOffset.UtcNow.Add(options.AbsoluteExpirationRelativeToNow ?? options.SlidingExpiration ?? TimeSpan.FromDays(1));
|
logger?.LogError(ex, "SetAsync({key})", key);
|
||||||
|
}
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>14.0</LangVersion>
|
||||||
<RootNamespace>MapControl.Caching</RootNamespace>
|
<RootNamespace>MapControl.Caching</RootNamespace>
|
||||||
<AssemblyTitle>XAML Map Control SQLiteCache Library</AssemblyTitle>
|
<AssemblyTitle>XAML Map Control SQLiteCache Library</AssemblyTitle>
|
||||||
<GeneratePackageOnBuild>$(GeneratePackage)</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>$(GeneratePackage)</GeneratePackageOnBuild>
|
||||||
|
|
|
||||||
|
|
@ -11,86 +11,85 @@ using Microsoft.UI.Xaml;
|
||||||
using DependencyProperty = Avalonia.AvaloniaProperty;
|
using DependencyProperty = Avalonia.AvaloniaProperty;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl.MBTiles
|
namespace MapControl.MBTiles;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MapTileLayer that uses an MBTiles SQLite Database. See https://wiki.openstreetmap.org/wiki/MBTiles.
|
||||||
|
/// </summary>
|
||||||
|
public class MBTileLayer : MapTileLayer
|
||||||
{
|
{
|
||||||
/// <summary>
|
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger<MBTileLayer>();
|
||||||
/// MapTileLayer that uses an MBTiles SQLite Database. See https://wiki.openstreetmap.org/wiki/MBTiles.
|
|
||||||
/// </summary>
|
public static readonly DependencyProperty FileProperty =
|
||||||
public class MBTileLayer : MapTileLayer
|
DependencyPropertyHelper.Register<MBTileLayer, string>(nameof(File), null,
|
||||||
|
async (layer, oldValue, newValue) => await layer.FilePropertyChanged(newValue));
|
||||||
|
|
||||||
|
public string File
|
||||||
{
|
{
|
||||||
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger<MBTileLayer>();
|
get => (string)GetValue(FileProperty);
|
||||||
|
set => SetValue(FileProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty FileProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MBTileLayer, string>(nameof(File), null,
|
/// May be overridden to create a derived MBTileSource that handles other tile formats than png and jpg.
|
||||||
async (layer, oldValue, newValue) => await layer.FilePropertyChanged(newValue));
|
/// </summary>
|
||||||
|
protected virtual async Task<MBTileSource> 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);
|
tileSource.Dispose();
|
||||||
set => SetValue(FileProperty, value);
|
|
||||||
|
throw new NotSupportedException($"Tile image format {format} is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return tileSource;
|
||||||
/// May be overridden to create a derived MBTileSource that handles other tile formats than png and jpg.
|
}
|
||||||
/// </summary>
|
|
||||||
protected virtual async Task<MBTileSource> CreateTileSourceAsync(string file)
|
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();
|
try
|
||||||
|
|
||||||
await tileSource.OpenAsync(file);
|
|
||||||
|
|
||||||
if (tileSource.Metadata.TryGetValue("format", out string format) && format != "png" && format != "jpg")
|
|
||||||
{
|
{
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
try
|
Logger?.LogError(ex, "Invalid file: {file}", file);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,74 +13,73 @@ using Microsoft.UI.Xaml.Media;
|
||||||
using ImageSource = Avalonia.Media.IImage;
|
using ImageSource = Avalonia.Media.IImage;
|
||||||
#endif
|
#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<MBTileSource>();
|
||||||
|
|
||||||
|
private SQLiteConnection connection;
|
||||||
|
|
||||||
|
public IDictionary<string, string> Metadata { get; } = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
public async Task OpenAsync(string file)
|
||||||
{
|
{
|
||||||
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger<MBTileSource>();
|
Close();
|
||||||
|
|
||||||
private SQLiteConnection connection;
|
connection = new SQLiteConnection("Data Source=" + FilePath.GetFullPath(file) + ";Read Only=True");
|
||||||
|
|
||||||
public IDictionary<string, string> Metadata { get; } = new Dictionary<string, string>();
|
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();
|
Metadata[(string)reader["name"]] = (string)reader["value"];
|
||||||
|
|
||||||
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<ImageSource> 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
if (connection != null)
|
||||||
|
{
|
||||||
|
Metadata.Clear();
|
||||||
|
connection.Dispose();
|
||||||
|
connection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<ImageSource> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,98 +6,97 @@ using System;
|
||||||
|
|
||||||
#pragma warning disable AVP1001
|
#pragma warning disable AVP1001
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public static class DependencyPropertyHelper
|
||||||
{
|
{
|
||||||
public static class DependencyPropertyHelper
|
public static AttachedProperty<TValue> RegisterAttached<TValue>(
|
||||||
|
string name,
|
||||||
|
Type ownerType,
|
||||||
|
TValue defaultValue = default,
|
||||||
|
Action<Control, TValue, TValue> changed = null,
|
||||||
|
bool inherits = false)
|
||||||
{
|
{
|
||||||
public static AttachedProperty<TValue> RegisterAttached<TValue>(
|
var property = AvaloniaProperty.RegisterAttached<Control, TValue>(name, ownerType, defaultValue, inherits);
|
||||||
string name,
|
|
||||||
Type ownerType,
|
if (changed != null)
|
||||||
TValue defaultValue = default,
|
|
||||||
Action<Control, TValue, TValue> changed = null,
|
|
||||||
bool inherits = false)
|
|
||||||
{
|
{
|
||||||
var property = AvaloniaProperty.RegisterAttached<Control, TValue>(name, ownerType, defaultValue, inherits);
|
property.Changed.AddClassHandler<Control, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
|
||||||
|
|
||||||
if (changed != null)
|
|
||||||
{
|
|
||||||
property.Changed.AddClassHandler<Control, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return property;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StyledProperty<TValue> Register<TOwner, TValue>(
|
return property;
|
||||||
string name,
|
}
|
||||||
TValue defaultValue = default,
|
|
||||||
Action<TOwner, TValue, TValue> changed = null,
|
public static StyledProperty<TValue> Register<TOwner, TValue>(
|
||||||
Func<TOwner, TValue, TValue> coerce = null,
|
string name,
|
||||||
bool bindTwoWayByDefault = false)
|
TValue defaultValue = default,
|
||||||
where TOwner : AvaloniaObject
|
Action<TOwner, TValue, TValue> changed = null,
|
||||||
|
Func<TOwner, TValue, TValue> coerce = null,
|
||||||
|
bool bindTwoWayByDefault = false)
|
||||||
|
where TOwner : AvaloniaObject
|
||||||
|
{
|
||||||
|
Func<AvaloniaObject, TValue, TValue> coerceFunc = null;
|
||||||
|
|
||||||
|
if (coerce != null)
|
||||||
{
|
{
|
||||||
Func<AvaloniaObject, TValue, TValue> coerceFunc = null;
|
// Do not coerce default value.
|
||||||
|
//
|
||||||
if (coerce != null)
|
coerceFunc = (obj, value) => Equals(value, defaultValue) ? value : coerce((TOwner)obj, value);
|
||||||
{
|
|
||||||
// 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<TOwner, TValue>(name, defaultValue, false, bindingMode, null, coerceFunc);
|
|
||||||
|
|
||||||
if (changed != null)
|
|
||||||
{
|
|
||||||
property.Changed.AddClassHandler<TOwner, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return property;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
|
var bindingMode = bindTwoWayByDefault ? BindingMode.TwoWay : BindingMode.OneWay;
|
||||||
StyledProperty<TValue> source) where TOwner : AvaloniaObject
|
|
||||||
{
|
|
||||||
return source.AddOwner<TOwner>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
|
var property = AvaloniaProperty.Register<TOwner, TValue>(name, defaultValue, false, bindingMode, null, coerceFunc);
|
||||||
StyledProperty<TValue> source,
|
|
||||||
TValue defaultValue) where TOwner : AvaloniaObject
|
|
||||||
{
|
|
||||||
return source.AddOwner<TOwner>(new StyledPropertyMetadata<TValue>(new Optional<TValue>(defaultValue)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
|
if (changed != null)
|
||||||
StyledProperty<TValue> source,
|
|
||||||
Action<TOwner, TValue, TValue> changed) where TOwner : AvaloniaObject
|
|
||||||
{
|
{
|
||||||
var property = source.AddOwner<TOwner>();
|
|
||||||
|
|
||||||
property.Changed.AddClassHandler<TOwner, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
|
property.Changed.AddClassHandler<TOwner, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
|
||||||
|
|
||||||
return property;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
|
return property;
|
||||||
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
|
}
|
||||||
StyledProperty<TValue> source) where TOwner : AvaloniaObject
|
|
||||||
{
|
|
||||||
return AddOwner<TOwner, TValue>(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
|
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
|
||||||
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
|
StyledProperty<TValue> source) where TOwner : AvaloniaObject
|
||||||
StyledProperty<TValue> source,
|
{
|
||||||
Action<TOwner, TValue, TValue> changed) where TOwner : AvaloniaObject
|
return source.AddOwner<TOwner>();
|
||||||
{
|
}
|
||||||
return AddOwner(source, changed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SetBinding(this AvaloniaObject target, AvaloniaProperty property, Binding binding)
|
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
|
||||||
{
|
StyledProperty<TValue> source,
|
||||||
target.Bind(property, binding);
|
TValue defaultValue) where TOwner : AvaloniaObject
|
||||||
}
|
{
|
||||||
|
return source.AddOwner<TOwner>(new StyledPropertyMetadata<TValue>(new Optional<TValue>(defaultValue)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
|
||||||
|
StyledProperty<TValue> source,
|
||||||
|
Action<TOwner, TValue, TValue> changed) where TOwner : AvaloniaObject
|
||||||
|
{
|
||||||
|
var property = source.AddOwner<TOwner>();
|
||||||
|
|
||||||
|
property.Changed.AddClassHandler<TOwner, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
|
||||||
|
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
|
||||||
|
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
|
||||||
|
StyledProperty<TValue> source) where TOwner : AvaloniaObject
|
||||||
|
{
|
||||||
|
return AddOwner<TOwner, TValue>(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
|
||||||
|
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
|
||||||
|
StyledProperty<TValue> source,
|
||||||
|
Action<TOwner, TValue, TValue> changed) where TOwner : AvaloniaObject
|
||||||
|
{
|
||||||
|
return AddOwner(source, changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetBinding(this AvaloniaObject target, AvaloniaProperty property, Binding binding)
|
||||||
|
{
|
||||||
|
target.Bind(property, binding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public static partial class GeoImage
|
||||||
{
|
{
|
||||||
public static partial class GeoImage
|
private static Task<GeoBitmap> LoadGeoTiff(string sourcePath)
|
||||||
{
|
{
|
||||||
private static Task<GeoBitmap> LoadGeoTiff(string sourcePath)
|
throw new InvalidOperationException("GeoTIFF is not supported.");
|
||||||
{
|
|
||||||
throw new InvalidOperationException("GeoTIFF is not supported.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,79 +7,78 @@ using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
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<IImage> LoadImageAsync(Stream stream)
|
||||||
|
{
|
||||||
|
return Thread.CurrentThread.IsThreadPoolThread ?
|
||||||
|
Task.FromResult(LoadImage(stream)) :
|
||||||
|
Task.Run(() => LoadImage(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<IImage> LoadImageAsync(string path)
|
||||||
|
{
|
||||||
|
return Thread.CurrentThread.IsThreadPoolThread ?
|
||||||
|
Task.FromResult(LoadImage(path)) :
|
||||||
|
Task.Run(() => LoadImage(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<IImage> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
|
||||||
|
{
|
||||||
|
Bitmap mergedBitmap = null;
|
||||||
|
var p1 = 0d;
|
||||||
|
var p2 = 0d;
|
||||||
|
|
||||||
|
var images = await Task.WhenAll(
|
||||||
|
LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
|
||||||
|
LoadImageAsync(uri2, new Progress<double>(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)
|
unsafe
|
||||||
{
|
|
||||||
return new Bitmap(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IImage LoadImage(string path)
|
|
||||||
{
|
|
||||||
return File.Exists(path) ? new Bitmap(path) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<IImage> LoadImageAsync(Stream stream)
|
|
||||||
{
|
|
||||||
return Thread.CurrentThread.IsThreadPoolThread ?
|
|
||||||
Task.FromResult(LoadImage(stream)) :
|
|
||||||
Task.Run(() => LoadImage(stream));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<IImage> LoadImageAsync(string path)
|
|
||||||
{
|
|
||||||
return Thread.CurrentThread.IsThreadPoolThread ?
|
|
||||||
Task.FromResult(LoadImage(path)) :
|
|
||||||
Task.Run(() => LoadImage(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static async Task<IImage> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
|
|
||||||
{
|
|
||||||
Bitmap mergedBitmap = null;
|
|
||||||
var p1 = 0d;
|
|
||||||
var p2 = 0d;
|
|
||||||
|
|
||||||
var images = await Task.WhenAll(
|
|
||||||
LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
|
|
||||||
LoadImageAsync(uri2, new Progress<double>(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)
|
|
||||||
{
|
{
|
||||||
var bpp = bitmap1.Format.Value == PixelFormat.Rgb565 ? 2 : 4;
|
fixed (byte* ptr = new byte[stride * pixelSize.Height])
|
||||||
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])
|
var buffer = (nint)ptr;
|
||||||
{
|
|
||||||
var buffer = (nint)ptr;
|
|
||||||
|
|
||||||
bitmap1.CopyPixels(new PixelRect(bitmap1.PixelSize), buffer, bufferSize, stride);
|
bitmap1.CopyPixels(new PixelRect(bitmap1.PixelSize), buffer, bufferSize, stride);
|
||||||
bitmap2.CopyPixels(new PixelRect(bitmap2.PixelSize), buffer + stride1, 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,46 +6,45 @@ using Avalonia.Styling;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
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)
|
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
|
||||||
: Tile(zoomLevel, x, y, columnCount)
|
|
||||||
|
public override async Task LoadImageAsync(Func<Task<IImage>> loadImageFunc)
|
||||||
{
|
{
|
||||||
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
|
var image = await loadImageFunc().ConfigureAwait(false);
|
||||||
|
|
||||||
public override async Task LoadImageAsync(Func<Task<IImage>> loadImageFunc)
|
void SetImageSource()
|
||||||
{
|
{
|
||||||
var image = await loadImageFunc().ConfigureAwait(false);
|
Image.Source = image;
|
||||||
|
|
||||||
void SetImageSource()
|
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
Image.Source = image;
|
var fadeInAnimation = new Animation
|
||||||
|
|
||||||
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
|
|
||||||
{
|
{
|
||||||
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) }
|
||||||
KeyTime = TimeSpan.Zero,
|
},
|
||||||
Setters = { new Setter(Visual.OpacityProperty, 0d) }
|
new KeyFrame
|
||||||
},
|
{
|
||||||
new KeyFrame
|
KeyTime = MapBase.ImageFadeDuration,
|
||||||
{
|
Setters = { new Setter(Visual.OpacityProperty, 1d) }
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
using Avalonia.Animation;
|
using Avalonia.Animation;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class LocationAnimator : InterpolatingAnimator<Location>
|
||||||
{
|
{
|
||||||
public class LocationAnimator : InterpolatingAnimator<Location>
|
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,
|
||||||
return new Location(
|
(1d - progress) * oldValue.Longitude + progress * newValue.Longitude);
|
||||||
(1d - progress) * oldValue.Latitude + progress * newValue.Latitude,
|
|
||||||
(1d - progress) * oldValue.Longitude + progress * newValue.Longitude);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,140 +2,139 @@
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum ManipulationModes
|
||||||
{
|
{
|
||||||
[Flags]
|
None = 0,
|
||||||
public enum ManipulationModes
|
Translate = 1,
|
||||||
|
Rotate = 2,
|
||||||
|
Scale = 4,
|
||||||
|
All = Translate | Rotate | Scale
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class Map
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<ManipulationModes> ManipulationModeProperty =
|
||||||
|
DependencyPropertyHelper.Register<Map, ManipulationModes>(nameof(ManipulationMode), ManipulationModes.Translate | ManipulationModes.Scale);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value that specifies how the map control handles manipulations.
|
||||||
|
/// </summary>
|
||||||
|
public ManipulationModes ManipulationMode
|
||||||
{
|
{
|
||||||
None = 0,
|
get => GetValue(ManipulationModeProperty);
|
||||||
Translate = 1,
|
set => SetValue(ManipulationModeProperty, value);
|
||||||
Rotate = 2,
|
|
||||||
Scale = 4,
|
|
||||||
All = Translate | Rotate | Scale
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class Map
|
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
|
||||||
{
|
{
|
||||||
public static readonly StyledProperty<ManipulationModes> ManipulationModeProperty =
|
base.OnPointerWheelChanged(e);
|
||||||
DependencyPropertyHelper.Register<Map, ManipulationModes>(nameof(ManipulationMode), ManipulationModes.Translate | ManipulationModes.Scale);
|
|
||||||
|
|
||||||
/// <summary>
|
OnMouseWheel(e.GetPosition(this), e.Delta.Y);
|
||||||
/// Gets or sets a value that specifies how the map control handles manipulations.
|
}
|
||||||
/// </summary>
|
|
||||||
public ManipulationModes ManipulationMode
|
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);
|
if (pointer2 != null)
|
||||||
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)
|
HandleManipulation(point.Pointer, point.Position);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (pointer1 == null &&
|
else if (point.Pointer.Type == PointerType.Mouse ||
|
||||||
point.Pointer.Type == PointerType.Mouse &&
|
ManipulationMode.HasFlag(ManipulationModes.Translate))
|
||||||
point.Properties.IsLeftButtonPressed &&
|
|
||||||
e.KeyModifiers == KeyModifiers.None ||
|
|
||||||
pointer2 == null &&
|
|
||||||
point.Pointer.Type == PointerType.Touch &&
|
|
||||||
ManipulationMode != ManipulationModes.None)
|
|
||||||
{
|
{
|
||||||
point.Pointer.Capture(this);
|
TranslateMap(new Point(point.Position.X - position1.X, point.Position.Y - position1.Y));
|
||||||
|
position1 = point.Position;
|
||||||
if (pointer1 == null)
|
|
||||||
{
|
|
||||||
pointer1 = point.Pointer;
|
|
||||||
position1 = point.Position;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pointer2 = point.Pointer;
|
|
||||||
position2 = point.Position;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (pointer1 == null &&
|
||||||
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
|
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 = point.Pointer;
|
||||||
{
|
position1 = point.Position;
|
||||||
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
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,266 +10,265 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Brush = Avalonia.Media.IBrush;
|
using Brush = Avalonia.Media.IBrush;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapBase
|
||||||
{
|
{
|
||||||
public partial class MapBase
|
public static readonly StyledProperty<Brush> ForegroundProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapBase, Brush>(TextElement.ForegroundProperty, Brushes.Black);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Easing> AnimationEasingProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, Easing>(nameof(AnimationEasing), new QuadraticEaseOut());
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Location> CenterProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, Location>(nameof(Center), new Location(0d, 0d),
|
||||||
|
(map, oldValue, newValue) => map.CenterPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceCenterProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Location> TargetCenterProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(0d, 0d),
|
||||||
|
async (map, oldValue, newValue) => await map.TargetCenterPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceCenterProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> MinZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d,
|
||||||
|
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceMinZoomLevelProperty(value));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> MaxZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d,
|
||||||
|
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceMaxZoomLevelProperty(value));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> ZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d,
|
||||||
|
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceZoomLevelProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> TargetZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d,
|
||||||
|
async (map, oldValue, newValue) => await map.TargetZoomLevelPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceZoomLevelProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> HeadingProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d,
|
||||||
|
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceHeadingProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> TargetHeadingProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d,
|
||||||
|
async (map, oldValue, newValue) => await map.TargetHeadingPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceHeadingProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly DirectProperty<MapBase, double> ViewScaleProperty =
|
||||||
|
AvaloniaProperty.RegisterDirect<MapBase, double>(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<Brush> ForegroundProperty =
|
BackgroundProperty.OverrideDefaultValue<MapBase>(Brushes.White);
|
||||||
DependencyPropertyHelper.AddOwner<MapBase, Brush>(TextElement.ForegroundProperty, Brushes.Black);
|
ClipToBoundsProperty.OverrideDefaultValue<MapBase>(true);
|
||||||
|
|
||||||
public static readonly StyledProperty<Easing> AnimationEasingProperty =
|
Animation.RegisterCustomAnimator<Location, LocationAnimator>();
|
||||||
DependencyPropertyHelper.Register<MapBase, Easing>(nameof(AnimationEasing), new QuadraticEaseOut());
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<Location> CenterProperty =
|
public double ActualWidth => Bounds.Width;
|
||||||
DependencyPropertyHelper.Register<MapBase, Location>(nameof(Center), new Location(0d, 0d),
|
public double ActualHeight => Bounds.Height;
|
||||||
(map, oldValue, newValue) => map.CenterPropertyChanged(newValue),
|
|
||||||
(map, value) => map.CoerceCenterProperty(value),
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<Location> TargetCenterProperty =
|
protected override void OnSizeChanged(SizeChangedEventArgs e)
|
||||||
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(0d, 0d),
|
{
|
||||||
async (map, oldValue, newValue) => await map.TargetCenterPropertyChanged(newValue),
|
base.OnSizeChanged(e);
|
||||||
(map, value) => map.CoerceCenterProperty(value),
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<double> MinZoomLevelProperty =
|
ResetTransformCenter();
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d,
|
UpdateTransform();
|
||||||
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue),
|
}
|
||||||
(map, value) => map.CoerceMinZoomLevelProperty(value));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<double> MaxZoomLevelProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d,
|
/// Gets or sets the Easing of the Center, ZoomLevel and Heading animations.
|
||||||
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue),
|
/// The default value is a QuadraticEaseOut.
|
||||||
(map, value) => map.CoerceMaxZoomLevelProperty(value));
|
/// </summary>
|
||||||
|
public Easing AnimationEasing
|
||||||
|
{
|
||||||
|
get => GetValue(AnimationEasingProperty);
|
||||||
|
set => SetValue(AnimationEasingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<double> ZoomLevelProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d,
|
/// Gets the scaling factor from projected map coordinates to view coordinates,
|
||||||
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue),
|
/// as pixels per meter.
|
||||||
(map, value) => map.CoerceZoomLevelProperty(value),
|
/// </summary>
|
||||||
true);
|
public double ViewScale
|
||||||
|
{
|
||||||
|
get => GetValue(ViewScaleProperty);
|
||||||
|
private set => RaisePropertyChanged(ViewScaleProperty, double.NaN, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<double> TargetZoomLevelProperty =
|
private void CenterPropertyChanged(Location center)
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d,
|
{
|
||||||
async (map, oldValue, newValue) => await map.TargetZoomLevelPropertyChanged(newValue),
|
if (!internalPropertyChange)
|
||||||
(map, value) => map.CoerceZoomLevelProperty(value),
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<double> HeadingProperty =
|
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d,
|
|
||||||
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue),
|
|
||||||
(map, value) => map.CoerceHeadingProperty(value),
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<double> TargetHeadingProperty =
|
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d,
|
|
||||||
async (map, oldValue, newValue) => await map.TargetHeadingPropertyChanged(newValue),
|
|
||||||
(map, value) => map.CoerceHeadingProperty(value),
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly DirectProperty<MapBase, double> ViewScaleProperty =
|
|
||||||
AvaloniaProperty.RegisterDirect<MapBase, double>(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()
|
|
||||||
{
|
{
|
||||||
BackgroundProperty.OverrideDefaultValue<MapBase>(Brushes.White);
|
|
||||||
ClipToBoundsProperty.OverrideDefaultValue<MapBase>(true);
|
|
||||||
|
|
||||||
Animation.RegisterCustomAnimator<Location, LocationAnimator>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public double ActualWidth => Bounds.Width;
|
|
||||||
public double ActualHeight => Bounds.Height;
|
|
||||||
|
|
||||||
protected override void OnSizeChanged(SizeChangedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnSizeChanged(e);
|
|
||||||
|
|
||||||
ResetTransformCenter();
|
|
||||||
UpdateTransform();
|
UpdateTransform();
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
if (centerAnimation == null)
|
||||||
/// Gets or sets the Easing of the Center, ZoomLevel and Heading animations.
|
|
||||||
/// The default value is a QuadraticEaseOut.
|
|
||||||
/// </summary>
|
|
||||||
public Easing AnimationEasing
|
|
||||||
{
|
|
||||||
get => GetValue(AnimationEasingProperty);
|
|
||||||
set => SetValue(AnimationEasingProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the scaling factor from projected map coordinates to view coordinates,
|
|
||||||
/// as pixels per meter.
|
|
||||||
/// </summary>
|
|
||||||
public double ViewScale
|
|
||||||
{
|
|
||||||
get => GetValue(ViewScaleProperty);
|
|
||||||
private set => RaisePropertyChanged(ViewScaleProperty, double.NaN, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CenterPropertyChanged(Location center)
|
|
||||||
{
|
|
||||||
if (!internalPropertyChange)
|
|
||||||
{
|
{
|
||||||
UpdateTransform();
|
SetValueInternal(TargetCenterProperty, center);
|
||||||
|
|
||||||
if (centerAnimation == null)
|
|
||||||
{
|
|
||||||
SetValueInternal(TargetCenterProperty, center);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private async Task TargetCenterPropertyChanged(Location targetCenter)
|
|
||||||
{
|
private async Task TargetCenterPropertyChanged(Location targetCenter)
|
||||||
if (!internalPropertyChange && !targetCenter.Equals(Center))
|
{
|
||||||
{
|
if (!internalPropertyChange && !targetCenter.Equals(Center))
|
||||||
ResetTransformCenter();
|
{
|
||||||
|
ResetTransformCenter();
|
||||||
centerCts?.Cancel();
|
|
||||||
|
centerCts?.Cancel();
|
||||||
centerAnimation = CreateAnimation(CenterProperty, new Location(targetCenter.Latitude, NearestLongitude(targetCenter.Longitude)));
|
|
||||||
|
centerAnimation = CreateAnimation(CenterProperty, new Location(targetCenter.Latitude, NearestLongitude(targetCenter.Longitude)));
|
||||||
using (centerCts = new CancellationTokenSource())
|
|
||||||
{
|
using (centerCts = new CancellationTokenSource())
|
||||||
await centerAnimation.RunAsync(this, centerCts.Token);
|
{
|
||||||
|
await centerAnimation.RunAsync(this, centerCts.Token);
|
||||||
if (!centerCts.IsCancellationRequested)
|
|
||||||
{
|
if (!centerCts.IsCancellationRequested)
|
||||||
UpdateTransform();
|
{
|
||||||
}
|
UpdateTransform();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
centerCts = null;
|
|
||||||
centerAnimation = null;
|
centerCts = null;
|
||||||
}
|
centerAnimation = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private void MinZoomLevelPropertyChanged(double minZoomLevel)
|
|
||||||
{
|
private void MinZoomLevelPropertyChanged(double minZoomLevel)
|
||||||
if (ZoomLevel < minZoomLevel)
|
{
|
||||||
{
|
if (ZoomLevel < minZoomLevel)
|
||||||
ZoomLevel = minZoomLevel;
|
{
|
||||||
}
|
ZoomLevel = minZoomLevel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private void MaxZoomLevelPropertyChanged(double maxZoomLevel)
|
|
||||||
{
|
private void MaxZoomLevelPropertyChanged(double maxZoomLevel)
|
||||||
if (ZoomLevel > maxZoomLevel)
|
{
|
||||||
{
|
if (ZoomLevel > maxZoomLevel)
|
||||||
ZoomLevel = maxZoomLevel;
|
{
|
||||||
}
|
ZoomLevel = maxZoomLevel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private void ZoomLevelPropertyChanged(double zoomLevel)
|
|
||||||
{
|
private void ZoomLevelPropertyChanged(double zoomLevel)
|
||||||
if (!internalPropertyChange)
|
{
|
||||||
{
|
if (!internalPropertyChange)
|
||||||
UpdateTransform();
|
{
|
||||||
|
UpdateTransform();
|
||||||
if (zoomLevelAnimation == null)
|
|
||||||
{
|
if (zoomLevelAnimation == null)
|
||||||
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
{
|
||||||
}
|
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private async Task TargetZoomLevelPropertyChanged(double targetZoomLevel)
|
|
||||||
{
|
private async Task TargetZoomLevelPropertyChanged(double targetZoomLevel)
|
||||||
if (!internalPropertyChange && targetZoomLevel != ZoomLevel)
|
{
|
||||||
{
|
if (!internalPropertyChange && targetZoomLevel != ZoomLevel)
|
||||||
zoomLevelCts?.Cancel();
|
{
|
||||||
|
zoomLevelCts?.Cancel();
|
||||||
zoomLevelAnimation = CreateAnimation(ZoomLevelProperty, targetZoomLevel);
|
|
||||||
|
zoomLevelAnimation = CreateAnimation(ZoomLevelProperty, targetZoomLevel);
|
||||||
using (zoomLevelCts = new CancellationTokenSource())
|
|
||||||
{
|
using (zoomLevelCts = new CancellationTokenSource())
|
||||||
await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token);
|
{
|
||||||
|
await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token);
|
||||||
if (!zoomLevelCts.IsCancellationRequested)
|
|
||||||
{
|
if (!zoomLevelCts.IsCancellationRequested)
|
||||||
UpdateTransform(true); // reset transform center
|
{
|
||||||
}
|
UpdateTransform(true); // reset transform center
|
||||||
}
|
}
|
||||||
|
}
|
||||||
zoomLevelCts = null;
|
|
||||||
zoomLevelAnimation = null;
|
zoomLevelCts = null;
|
||||||
}
|
zoomLevelAnimation = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private void HeadingPropertyChanged(double heading)
|
|
||||||
{
|
private void HeadingPropertyChanged(double heading)
|
||||||
if (!internalPropertyChange)
|
{
|
||||||
{
|
if (!internalPropertyChange)
|
||||||
UpdateTransform();
|
{
|
||||||
|
UpdateTransform();
|
||||||
if (headingAnimation == null)
|
|
||||||
{
|
if (headingAnimation == null)
|
||||||
SetValueInternal(TargetHeadingProperty, heading);
|
{
|
||||||
}
|
SetValueInternal(TargetHeadingProperty, heading);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private async Task TargetHeadingPropertyChanged(double targetHeading)
|
|
||||||
{
|
private async Task TargetHeadingPropertyChanged(double targetHeading)
|
||||||
if (!internalPropertyChange && targetHeading != Heading)
|
{
|
||||||
{
|
if (!internalPropertyChange && targetHeading != Heading)
|
||||||
var delta = targetHeading - Heading;
|
{
|
||||||
|
var delta = targetHeading - Heading;
|
||||||
if (delta > 180d)
|
|
||||||
{
|
if (delta > 180d)
|
||||||
delta -= 360d;
|
{
|
||||||
}
|
delta -= 360d;
|
||||||
else if (delta < -180d)
|
}
|
||||||
{
|
else if (delta < -180d)
|
||||||
delta += 360d;
|
{
|
||||||
}
|
delta += 360d;
|
||||||
|
}
|
||||||
targetHeading = Heading + delta;
|
|
||||||
|
targetHeading = Heading + delta;
|
||||||
headingCts?.Cancel();
|
|
||||||
|
headingCts?.Cancel();
|
||||||
headingAnimation = CreateAnimation(HeadingProperty, targetHeading);
|
|
||||||
|
headingAnimation = CreateAnimation(HeadingProperty, targetHeading);
|
||||||
using (headingCts = new CancellationTokenSource())
|
|
||||||
{
|
using (headingCts = new CancellationTokenSource())
|
||||||
await headingAnimation.RunAsync(this, headingCts.Token);
|
{
|
||||||
|
await headingAnimation.RunAsync(this, headingCts.Token);
|
||||||
if (!headingCts.IsCancellationRequested)
|
|
||||||
{
|
if (!headingCts.IsCancellationRequested)
|
||||||
UpdateTransform();
|
{
|
||||||
}
|
UpdateTransform();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
headingCts = null;
|
|
||||||
headingAnimation = null;
|
headingCts = null;
|
||||||
}
|
headingAnimation = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private Animation CreateAnimation(DependencyProperty property, object value)
|
|
||||||
{
|
private Animation CreateAnimation(DependencyProperty property, object value)
|
||||||
return new Animation
|
{
|
||||||
{
|
return new Animation
|
||||||
FillMode = FillMode.Forward,
|
{
|
||||||
Duration = AnimationDuration,
|
FillMode = FillMode.Forward,
|
||||||
Easing = AnimationEasing,
|
Duration = AnimationDuration,
|
||||||
Children =
|
Easing = AnimationEasing,
|
||||||
{
|
Children =
|
||||||
new KeyFrame
|
{
|
||||||
{
|
new KeyFrame
|
||||||
KeyTime = AnimationDuration,
|
{
|
||||||
Setters = { new Setter(property, value) }
|
KeyTime = AnimationDuration,
|
||||||
}
|
Setters = { new Setter(property, value) }
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
|
|
||||||
namespace MapControl
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -7,116 +7,115 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapGrid : Control, IMapElement
|
||||||
{
|
{
|
||||||
public partial class MapGrid : Control, IMapElement
|
static MapGrid()
|
||||||
{
|
{
|
||||||
static MapGrid()
|
AffectsRender<MapGrid>(ForegroundProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush> ForegroundProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapGrid, IBrush>(TextElement.ForegroundProperty);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<FontFamily> FontFamilyProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapGrid, FontFamily>(TextElement.FontFamilyProperty);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> FontSizeProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapGrid, double>(TextElement.FontSizeProperty, 12d);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements IMapElement.ParentMap.
|
||||||
|
/// </summary>
|
||||||
|
public MapBase ParentMap
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set
|
||||||
{
|
{
|
||||||
AffectsRender<MapGrid>(ForegroundProperty);
|
if (field != null)
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> ForegroundProperty =
|
|
||||||
DependencyPropertyHelper.AddOwner<MapGrid, IBrush>(TextElement.ForegroundProperty);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<FontFamily> FontFamilyProperty =
|
|
||||||
DependencyPropertyHelper.AddOwner<MapGrid, FontFamily>(TextElement.FontFamilyProperty);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<double> FontSizeProperty =
|
|
||||||
DependencyPropertyHelper.AddOwner<MapGrid, double>(TextElement.FontSizeProperty, 12d);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implements IMapElement.ParentMap.
|
|
||||||
/// </summary>
|
|
||||||
public MapBase ParentMap
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set
|
|
||||||
{
|
{
|
||||||
if (field != null)
|
field.ViewportChanged -= OnViewportChanged;
|
||||||
{
|
}
|
||||||
field.ViewportChanged -= OnViewportChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
field = value;
|
field = value;
|
||||||
|
|
||||||
if (field != null)
|
if (field != null)
|
||||||
{
|
{
|
||||||
field.ViewportChanged += OnViewportChanged;
|
field.ViewportChanged += OnViewportChanged;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
||||||
{
|
{
|
||||||
OnViewportChanged(e);
|
OnViewportChanged(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
|
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
|
||||||
{
|
{
|
||||||
InvalidateVisual();
|
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<Label>();
|
||||||
|
var pen = new Pen
|
||||||
{
|
{
|
||||||
var pathGeometry = new PathGeometry();
|
Brush = Foreground,
|
||||||
var labels = new List<Label>();
|
Thickness = StrokeThickness,
|
||||||
var pen = new Pen
|
};
|
||||||
|
|
||||||
|
DrawGrid(pathGeometry.Figures, labels);
|
||||||
|
|
||||||
|
drawingContext.DrawGeometry(null, pen, pathGeometry);
|
||||||
|
|
||||||
|
if (labels.Count > 0)
|
||||||
|
{
|
||||||
|
var typeface = new Typeface(FontFamily, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal);
|
||||||
|
|
||||||
|
foreach (var label in labels)
|
||||||
{
|
{
|
||||||
Brush = Foreground,
|
var text = new FormattedText(label.Text,
|
||||||
Thickness = StrokeThickness,
|
CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, FontSize, Foreground);
|
||||||
};
|
var x = label.X +
|
||||||
|
label.HorizontalAlignment switch
|
||||||
|
{
|
||||||
|
HorizontalAlignment.Left => 2d,
|
||||||
|
HorizontalAlignment.Right => -text.Width - 2d,
|
||||||
|
_ => -text.Width / 2d
|
||||||
|
};
|
||||||
|
var y = label.Y +
|
||||||
|
label.VerticalAlignment switch
|
||||||
|
{
|
||||||
|
VerticalAlignment.Top => 0,
|
||||||
|
VerticalAlignment.Bottom => -text.Height,
|
||||||
|
_ => -text.Height / 2d
|
||||||
|
};
|
||||||
|
|
||||||
DrawGrid(pathGeometry.Figures, labels);
|
if (label.Rotation != 0d)
|
||||||
|
|
||||||
drawingContext.DrawGeometry(null, pen, pathGeometry);
|
|
||||||
|
|
||||||
if (labels.Count > 0)
|
|
||||||
{
|
|
||||||
var typeface = new Typeface(FontFamily, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal);
|
|
||||||
|
|
||||||
foreach (var label in labels)
|
|
||||||
{
|
{
|
||||||
var text = new FormattedText(label.Text,
|
var transform = Avalonia.Matrix.CreateRotation(
|
||||||
CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, FontSize, Foreground);
|
label.Rotation * Math.PI / 180d, new Point(label.X, label.Y));
|
||||||
var x = label.X +
|
|
||||||
label.HorizontalAlignment switch
|
|
||||||
{
|
|
||||||
HorizontalAlignment.Left => 2d,
|
|
||||||
HorizontalAlignment.Right => -text.Width - 2d,
|
|
||||||
_ => -text.Width / 2d
|
|
||||||
};
|
|
||||||
var y = label.Y +
|
|
||||||
label.VerticalAlignment switch
|
|
||||||
{
|
|
||||||
VerticalAlignment.Top => 0,
|
|
||||||
VerticalAlignment.Bottom => -text.Height,
|
|
||||||
_ => -text.Height / 2d
|
|
||||||
};
|
|
||||||
|
|
||||||
if (label.Rotation != 0d)
|
using var pushedState = drawingContext.PushTransform(transform);
|
||||||
{
|
|
||||||
var transform = Avalonia.Matrix.CreateRotation(
|
|
||||||
label.Rotation * Math.PI / 180d, new Point(label.X, label.Y));
|
|
||||||
|
|
||||||
using var pushedState = drawingContext.PushTransform(transform);
|
drawingContext.DrawText(text, new Point(x, y));
|
||||||
|
}
|
||||||
drawingContext.DrawText(text, new Point(x, y));
|
else
|
||||||
}
|
{
|
||||||
else
|
drawingContext.DrawText(text, new Point(x, y));
|
||||||
{
|
|
||||||
drawingContext.DrawText(text, new Point(x, y));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
|
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
|
||||||
{
|
{
|
||||||
return new PolyLineSegment(points);
|
return new PolyLineSegment(points);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,28 @@
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
{
|
|
||||||
public partial class MapImageLayer
|
|
||||||
{
|
|
||||||
private void FadeOver()
|
|
||||||
{
|
|
||||||
var fadeInAnimation = new Animation
|
|
||||||
{
|
|
||||||
FillMode = FillMode.Forward,
|
|
||||||
Duration = MapBase.ImageFadeDuration,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new KeyFrame
|
|
||||||
{
|
|
||||||
KeyTime = MapBase.ImageFadeDuration,
|
|
||||||
Setters = { new Setter(OpacityProperty, 1d) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = fadeInAnimation.RunAsync(Children[1]).ContinueWith(
|
public partial class MapImageLayer
|
||||||
_ => Children[0].Opacity = 0d,
|
{
|
||||||
TaskScheduler.FromCurrentSynchronizationContext());
|
private void FadeOver()
|
||||||
}
|
{
|
||||||
|
var fadeInAnimation = new Animation
|
||||||
|
{
|
||||||
|
FillMode = FillMode.Forward,
|
||||||
|
Duration = MapBase.ImageFadeDuration,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new KeyFrame
|
||||||
|
{
|
||||||
|
KeyTime = MapBase.ImageFadeDuration,
|
||||||
|
Setters = { new Setter(OpacityProperty, 1d) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = fadeInAnimation.RunAsync(Children[1]).ContinueWith(
|
||||||
|
_ => Children[0].Opacity = 0d,
|
||||||
|
TaskScheduler.FromCurrentSynchronizationContext());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,30 @@
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapItem
|
||||||
{
|
{
|
||||||
public partial class MapItem
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||||
{
|
{
|
||||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
if (e.Pointer.Type != PointerType.Mouse &&
|
||||||
|
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl)
|
||||||
{
|
{
|
||||||
if (e.Pointer.Type != PointerType.Mouse &&
|
mapItemsControl.UpdateSelectionFromEvent(this, e);
|
||||||
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl)
|
|
||||||
{
|
|
||||||
mapItemsControl.UpdateSelectionFromEvent(this, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
e.Handled = true;
|
||||||
{
|
}
|
||||||
if (e.Pointer.Type == PointerType.Mouse &&
|
|
||||||
e.InitialPressMouseButton == MouseButton.Left &&
|
|
||||||
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl)
|
|
||||||
{
|
|
||||||
mapItemsControl.UpdateSelectionFromEvent(this, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Handled = true;
|
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Pointer.Type == PointerType.Mouse &&
|
||||||
|
e.InitialPressMouseButton == MouseButton.Left &&
|
||||||
|
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl)
|
||||||
|
{
|
||||||
|
mapItemsControl.UpdateSelectionFromEvent(this, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,70 +6,69 @@ using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapItemsControl
|
||||||
{
|
{
|
||||||
public partial class MapItemsControl
|
static MapItemsControl()
|
||||||
{
|
{
|
||||||
static MapItemsControl()
|
TemplateProperty.OverrideDefaultValue<MapItemsControl>(
|
||||||
{
|
new FuncControlTemplate<MapItemsControl>(
|
||||||
TemplateProperty.OverrideDefaultValue<MapItemsControl>(
|
(itemsControl, namescope) => new ItemsPresenter { ItemsPanel = itemsControl.ItemsPanel }));
|
||||||
new FuncControlTemplate<MapItemsControl>(
|
|
||||||
(itemsControl, namescope) => new ItemsPresenter { ItemsPanel = itemsControl.ItemsPanel }));
|
ItemsPanelProperty.OverrideDefaultValue<MapItemsControl>(
|
||||||
|
new FuncTemplate<Panel>(() => new MapPanel()));
|
||||||
ItemsPanelProperty.OverrideDefaultValue<MapItemsControl>(
|
}
|
||||||
new FuncTemplate<Panel>(() => new MapPanel()));
|
|
||||||
}
|
public void SelectItemsInGeometry(Geometry geometry)
|
||||||
|
{
|
||||||
public void SelectItemsInGeometry(Geometry geometry)
|
SelectItemsByPosition(geometry.FillContains);
|
||||||
{
|
}
|
||||||
SelectItemsByPosition(geometry.FillContains);
|
|
||||||
}
|
public new MapItem ContainerFromItem(object item)
|
||||||
|
{
|
||||||
public new MapItem ContainerFromItem(object item)
|
return (MapItem)base.ContainerFromItem(item);
|
||||||
{
|
}
|
||||||
return (MapItem)base.ContainerFromItem(item);
|
|
||||||
}
|
protected override bool NeedsContainerOverride(object item, int index, out object recycleKey)
|
||||||
|
{
|
||||||
protected override bool NeedsContainerOverride(object item, int index, out object recycleKey)
|
recycleKey = null;
|
||||||
{
|
|
||||||
recycleKey = null;
|
return item is not MapItem;
|
||||||
|
}
|
||||||
return item is not MapItem;
|
|
||||||
}
|
protected override Control CreateContainerForItemOverride(object item, int index, object recycleKey)
|
||||||
|
{
|
||||||
protected override Control CreateContainerForItemOverride(object item, int index, object recycleKey)
|
return new MapItem();
|
||||||
{
|
}
|
||||||
return new MapItem();
|
|
||||||
}
|
protected override void PrepareContainerForItemOverride(Control container, object item, int index)
|
||||||
|
{
|
||||||
protected override void PrepareContainerForItemOverride(Control container, object item, int index)
|
base.PrepareContainerForItemOverride(container, item, index);
|
||||||
{
|
PrepareContainer((MapItem)container, item);
|
||||||
base.PrepareContainerForItemOverride(container, item, index);
|
}
|
||||||
PrepareContainer((MapItem)container, item);
|
|
||||||
}
|
protected override void ClearContainerForItemOverride(Control container)
|
||||||
|
{
|
||||||
protected override void ClearContainerForItemOverride(Control container)
|
base.ClearContainerForItemOverride(container);
|
||||||
{
|
ClearContainer((MapItem)container);
|
||||||
base.ClearContainerForItemOverride(container);
|
}
|
||||||
ClearContainer((MapItem)container);
|
|
||||||
}
|
protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs)
|
||||||
|
{
|
||||||
protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs)
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool UpdateSelectionFromEvent(UIElement container, RoutedEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
if (SelectionMode == SelectionMode.Multiple &&
|
||||||
|
eventArgs is PointerEventArgs e &&
|
||||||
|
e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
||||||
{
|
{
|
||||||
|
SelectItemsInRange((MapItem)container);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool UpdateSelectionFromEvent(UIElement container, RoutedEventArgs eventArgs)
|
return base.UpdateSelectionFromEvent(container, eventArgs);
|
||||||
{
|
|
||||||
if (SelectionMode == SelectionMode.Multiple &&
|
|
||||||
eventArgs is PointerEventArgs e &&
|
|
||||||
e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
|
||||||
{
|
|
||||||
SelectItemsInRange((MapItem)container);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.UpdateSelectionFromEvent(container, eventArgs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,28 @@
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapPanel
|
||||||
{
|
{
|
||||||
public partial class MapPanel
|
public static readonly AttachedProperty<bool> AutoCollapseProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<bool>("AutoCollapse", typeof(MapPanel));
|
||||||
|
|
||||||
|
public static readonly AttachedProperty<Location> LocationProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<Location>("Location", typeof(MapPanel));
|
||||||
|
|
||||||
|
public static readonly AttachedProperty<BoundingBox> BoundingBoxProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel));
|
||||||
|
|
||||||
|
public static readonly AttachedProperty<Rect?> MapRectProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<Rect?>("MapRect", typeof(MapPanel));
|
||||||
|
|
||||||
|
static MapPanel()
|
||||||
{
|
{
|
||||||
public static readonly AttachedProperty<bool> AutoCollapseProperty =
|
AffectsParentArrange<MapPanel>(LocationProperty, BoundingBoxProperty, MapRectProperty);
|
||||||
DependencyPropertyHelper.RegisterAttached<bool>("AutoCollapse", typeof(MapPanel));
|
}
|
||||||
|
|
||||||
public static readonly AttachedProperty<Location> LocationProperty =
|
public static MapBase GetParentMap(FrameworkElement element)
|
||||||
DependencyPropertyHelper.RegisterAttached<Location>("Location", typeof(MapPanel));
|
{
|
||||||
|
return (MapBase)element.GetValue(ParentMapProperty);
|
||||||
public static readonly AttachedProperty<BoundingBox> BoundingBoxProperty =
|
|
||||||
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel));
|
|
||||||
|
|
||||||
public static readonly AttachedProperty<Rect?> MapRectProperty =
|
|
||||||
DependencyPropertyHelper.RegisterAttached<Rect?>("MapRect", typeof(MapPanel));
|
|
||||||
|
|
||||||
static MapPanel()
|
|
||||||
{
|
|
||||||
AffectsParentArrange<MapPanel>(LocationProperty, BoundingBoxProperty, MapRectProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MapBase GetParentMap(FrameworkElement element)
|
|
||||||
{
|
|
||||||
return (MapBase)element.GetValue(ParentMapProperty);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,19 @@
|
||||||
using Avalonia.Controls.Shapes;
|
using Avalonia.Controls.Shapes;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapPath : Shape
|
||||||
{
|
{
|
||||||
public partial class MapPath : Shape
|
public static readonly StyledProperty<Geometry> DataProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapPath, Geometry>(Path.DataProperty,
|
||||||
|
(path, oldValue, newValue) => path.UpdateData());
|
||||||
|
|
||||||
|
public Geometry Data
|
||||||
{
|
{
|
||||||
public static readonly StyledProperty<Geometry> DataProperty =
|
get => GetValue(DataProperty);
|
||||||
DependencyPropertyHelper.AddOwner<MapPath, Geometry>(Path.DataProperty,
|
set => SetValue(DataProperty, value);
|
||||||
(path, oldValue, newValue) => path.UpdateData());
|
|
||||||
|
|
||||||
public Geometry Data
|
|
||||||
{
|
|
||||||
get => GetValue(DataProperty);
|
|
||||||
set => SetValue(DataProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Geometry CreateDefiningGeometry() => Data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override Geometry CreateDefiningGeometry() => Data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,89 +4,88 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapPolypoint : MapPath
|
||||||
{
|
{
|
||||||
public partial class MapPolypoint : MapPath
|
protected void UpdateData(IEnumerable<Location> locations, bool closed)
|
||||||
{
|
{
|
||||||
protected void UpdateData(IEnumerable<Location> locations, bool closed)
|
var figures = new PathFigures();
|
||||||
|
|
||||||
|
if (ParentMap != null && locations != null)
|
||||||
{
|
{
|
||||||
var figures = new PathFigures();
|
var longitudeOffset = GetLongitudeOffset(locations);
|
||||||
|
|
||||||
if (ParentMap != null && locations != null)
|
AddPolylinePoints(figures, locations, longitudeOffset, closed);
|
||||||
{
|
|
||||||
var longitudeOffset = GetLongitudeOffset(locations);
|
|
||||||
|
|
||||||
AddPolylinePoints(figures, locations, longitudeOffset, closed);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetPathFigures(figures);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void UpdateData(IEnumerable<IEnumerable<Location>> polygons)
|
SetPathFigures(figures);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdateData(IEnumerable<IEnumerable<Location>> polygons)
|
||||||
|
{
|
||||||
|
var figures = new PathFigures();
|
||||||
|
|
||||||
|
if (ParentMap != null && polygons != null)
|
||||||
{
|
{
|
||||||
var figures = new PathFigures();
|
var longitudeOffset = GetLongitudeOffset(polygons.FirstOrDefault());
|
||||||
|
|
||||||
if (ParentMap != null && polygons != null)
|
foreach (var locations in polygons)
|
||||||
{
|
{
|
||||||
var longitudeOffset = GetLongitudeOffset(polygons.FirstOrDefault());
|
AddPolylinePoints(figures, locations, longitudeOffset, true);
|
||||||
|
|
||||||
foreach (var locations in polygons)
|
|
||||||
{
|
|
||||||
AddPolylinePoints(figures, locations, longitudeOffset, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SetPathFigures(figures);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddPolylinePoints(PathFigures figures, IEnumerable<Location> locations, double longitudeOffset, bool closed)
|
|
||||||
{
|
|
||||||
var points = locations.Select(location => LocationToView(location, longitudeOffset));
|
|
||||||
|
|
||||||
if (points.Any())
|
|
||||||
{
|
|
||||||
var start = points.First();
|
|
||||||
var polyline = new PolyLineSegment(points.Skip(1));
|
|
||||||
var minX = start.X;
|
|
||||||
var maxX = start.X;
|
|
||||||
var minY = start.Y;
|
|
||||||
var maxY = start.Y;
|
|
||||||
|
|
||||||
foreach (var point in polyline.Points)
|
|
||||||
{
|
|
||||||
minX = Math.Min(minX, point.X);
|
|
||||||
maxX = Math.Max(maxX, point.X);
|
|
||||||
minY = Math.Min(minY, point.Y);
|
|
||||||
maxY = Math.Max(maxY, point.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxX >= 0d && minX <= ParentMap.ActualWidth &&
|
|
||||||
maxY >= 0d && minY <= ParentMap.ActualHeight)
|
|
||||||
{
|
|
||||||
var figure = new PathFigure
|
|
||||||
{
|
|
||||||
StartPoint = start,
|
|
||||||
IsClosed = closed,
|
|
||||||
IsFilled = true
|
|
||||||
};
|
|
||||||
|
|
||||||
figure.Segments.Add(polyline);
|
|
||||||
figures.Add(figure);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetPathFigures(PathFigures figures)
|
SetPathFigures(figures);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddPolylinePoints(PathFigures figures, IEnumerable<Location> locations, double longitudeOffset, bool closed)
|
||||||
|
{
|
||||||
|
var points = locations.Select(location => LocationToView(location, longitudeOffset));
|
||||||
|
|
||||||
|
if (points.Any())
|
||||||
{
|
{
|
||||||
if (figures.Count == 0)
|
var start = points.First();
|
||||||
|
var polyline = new PolyLineSegment(points.Skip(1));
|
||||||
|
var minX = start.X;
|
||||||
|
var maxX = start.X;
|
||||||
|
var minY = start.Y;
|
||||||
|
var maxY = start.Y;
|
||||||
|
|
||||||
|
foreach (var point in polyline.Points)
|
||||||
{
|
{
|
||||||
// Avalonia Shape seems to ignore PathGeometry with empty Figures collection.
|
minX = Math.Min(minX, point.X);
|
||||||
//
|
maxX = Math.Max(maxX, point.X);
|
||||||
figures.Add(new PathFigure { StartPoint = new Point(-1000, -1000) });
|
minY = Math.Min(minY, point.Y);
|
||||||
|
maxY = Math.Max(maxY, point.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
((PathGeometry)Data).Figures = figures;
|
if (maxX >= 0d && minX <= ParentMap.ActualWidth &&
|
||||||
InvalidateGeometry();
|
maxY >= 0d && minY <= ParentMap.ActualHeight)
|
||||||
|
{
|
||||||
|
var figure = new PathFigure
|
||||||
|
{
|
||||||
|
StartPoint = start,
|
||||||
|
IsClosed = closed,
|
||||||
|
IsFilled = true
|
||||||
|
};
|
||||||
|
|
||||||
|
figure.Segments.Add(polyline);
|
||||||
|
figures.Add(figure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetPathFigures(PathFigures figures)
|
||||||
|
{
|
||||||
|
if (figures.Count == 0)
|
||||||
|
{
|
||||||
|
// Avalonia Shape seems to ignore PathGeometry with empty Figures collection.
|
||||||
|
//
|
||||||
|
figures.Add(new PathFigure { StartPoint = new Point(-1000, -1000) });
|
||||||
|
}
|
||||||
|
|
||||||
|
((PathGeometry)Data).Figures = figures;
|
||||||
|
InvalidateGeometry();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,100 +4,99 @@ using Avalonia.Media;
|
||||||
using System;
|
using System;
|
||||||
using Brush = Avalonia.Media.IBrush;
|
using Brush = Avalonia.Media.IBrush;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class PushpinBorder : Decorator
|
||||||
{
|
{
|
||||||
public partial class PushpinBorder : Decorator
|
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
|
||||||
|
DependencyPropertyHelper.Register<PushpinBorder, CornerRadius>(nameof(CornerRadius), new CornerRadius());
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Size> ArrowSizeProperty =
|
||||||
|
DependencyPropertyHelper.Register<PushpinBorder, Size>(nameof(ArrowSize), new Size(10d, 20d));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> BorderWidthProperty =
|
||||||
|
DependencyPropertyHelper.Register<PushpinBorder, double>(nameof(BorderWidth));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Brush> BackgroundProperty =
|
||||||
|
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(Background));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Brush> BorderBrushProperty =
|
||||||
|
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(BorderBrush));
|
||||||
|
|
||||||
|
static PushpinBorder()
|
||||||
{
|
{
|
||||||
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
|
AffectsMeasure<PushpinBorder>(ArrowSizeProperty, BorderWidthProperty, CornerRadiusProperty);
|
||||||
DependencyPropertyHelper.Register<PushpinBorder, CornerRadius>(nameof(CornerRadius), new CornerRadius());
|
AffectsRender<PushpinBorder>(BackgroundProperty, BorderBrushProperty);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<Size> ArrowSizeProperty =
|
public double ActualWidth => Bounds.Width;
|
||||||
DependencyPropertyHelper.Register<PushpinBorder, Size>(nameof(ArrowSize), new Size(10d, 20d));
|
public double ActualHeight => Bounds.Height;
|
||||||
|
|
||||||
public static readonly StyledProperty<double> BorderWidthProperty =
|
public CornerRadius CornerRadius
|
||||||
DependencyPropertyHelper.Register<PushpinBorder, double>(nameof(BorderWidth));
|
{
|
||||||
|
get => GetValue(CornerRadiusProperty);
|
||||||
|
set => SetValue(CornerRadiusProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<Brush> BackgroundProperty =
|
public Brush Background
|
||||||
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(Background));
|
{
|
||||||
|
get => GetValue(BackgroundProperty);
|
||||||
|
set => SetValue(BackgroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<Brush> BorderBrushProperty =
|
public Brush BorderBrush
|
||||||
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(BorderBrush));
|
{
|
||||||
|
get => GetValue(BorderBrushProperty);
|
||||||
|
set => SetValue(BorderBrushProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
static PushpinBorder()
|
protected override Size MeasureOverride(Size constraint)
|
||||||
|
{
|
||||||
|
var width = 2d * BorderWidth + Padding.Left + Padding.Right;
|
||||||
|
var height = 2d * BorderWidth + Padding.Top + Padding.Bottom;
|
||||||
|
|
||||||
|
if (Child != null)
|
||||||
{
|
{
|
||||||
AffectsMeasure<PushpinBorder>(ArrowSizeProperty, BorderWidthProperty, CornerRadiusProperty);
|
Child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||||
AffectsRender<PushpinBorder>(BackgroundProperty, BorderBrushProperty);
|
width += Child.DesiredSize.Width;
|
||||||
|
height += Child.DesiredSize.Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double ActualWidth => Bounds.Width;
|
var minWidth = BorderWidth + Math.Max(
|
||||||
public double ActualHeight => Bounds.Height;
|
CornerRadius.TopLeft + CornerRadius.TopRight,
|
||||||
|
CornerRadius.BottomLeft + CornerRadius.BottomRight + ArrowSize.Width);
|
||||||
|
|
||||||
public CornerRadius CornerRadius
|
var minHeight = BorderWidth + Math.Max(
|
||||||
|
CornerRadius.TopLeft + CornerRadius.BottomLeft,
|
||||||
|
CornerRadius.TopRight + CornerRadius.BottomRight);
|
||||||
|
|
||||||
|
return new Size(
|
||||||
|
Math.Max(width, minWidth),
|
||||||
|
Math.Max(height, minHeight) + ArrowSize.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size ArrangeOverride(Size size)
|
||||||
|
{
|
||||||
|
Child?.Arrange(new Rect(
|
||||||
|
BorderWidth + Padding.Left,
|
||||||
|
BorderWidth + Padding.Top,
|
||||||
|
Child.DesiredSize.Width,
|
||||||
|
Child.DesiredSize.Height));
|
||||||
|
|
||||||
|
return DesiredSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Render(DrawingContext drawingContext)
|
||||||
|
{
|
||||||
|
var pen = new Pen
|
||||||
{
|
{
|
||||||
get => GetValue(CornerRadiusProperty);
|
Brush = BorderBrush,
|
||||||
set => SetValue(CornerRadiusProperty, value);
|
Thickness = BorderWidth,
|
||||||
}
|
LineJoin = PenLineJoin.Round
|
||||||
|
};
|
||||||
|
|
||||||
public Brush Background
|
drawingContext.DrawGeometry(Background, pen, BuildGeometry());
|
||||||
{
|
|
||||||
get => GetValue(BackgroundProperty);
|
|
||||||
set => SetValue(BackgroundProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Brush BorderBrush
|
base.Render(drawingContext);
|
||||||
{
|
|
||||||
get => GetValue(BorderBrushProperty);
|
|
||||||
set => SetValue(BorderBrushProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Size MeasureOverride(Size constraint)
|
|
||||||
{
|
|
||||||
var width = 2d * BorderWidth + Padding.Left + Padding.Right;
|
|
||||||
var height = 2d * BorderWidth + Padding.Top + Padding.Bottom;
|
|
||||||
|
|
||||||
if (Child != null)
|
|
||||||
{
|
|
||||||
Child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
|
||||||
width += Child.DesiredSize.Width;
|
|
||||||
height += Child.DesiredSize.Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
var minWidth = BorderWidth + Math.Max(
|
|
||||||
CornerRadius.TopLeft + CornerRadius.TopRight,
|
|
||||||
CornerRadius.BottomLeft + CornerRadius.BottomRight + ArrowSize.Width);
|
|
||||||
|
|
||||||
var minHeight = BorderWidth + Math.Max(
|
|
||||||
CornerRadius.TopLeft + CornerRadius.BottomLeft,
|
|
||||||
CornerRadius.TopRight + CornerRadius.BottomRight);
|
|
||||||
|
|
||||||
return new Size(
|
|
||||||
Math.Max(width, minWidth),
|
|
||||||
Math.Max(height, minHeight) + ArrowSize.Height);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Size ArrangeOverride(Size size)
|
|
||||||
{
|
|
||||||
Child?.Arrange(new Rect(
|
|
||||||
BorderWidth + Padding.Left,
|
|
||||||
BorderWidth + Padding.Top,
|
|
||||||
Child.DesiredSize.Width,
|
|
||||||
Child.DesiredSize.Height));
|
|
||||||
|
|
||||||
return DesiredSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Render(DrawingContext drawingContext)
|
|
||||||
{
|
|
||||||
var pen = new Pen
|
|
||||||
{
|
|
||||||
Brush = BorderBrush,
|
|
||||||
Thickness = BorderWidth,
|
|
||||||
LineJoin = PenLineJoin.Round
|
|
||||||
};
|
|
||||||
|
|
||||||
drawingContext.DrawGeometry(Background, pen, BuildGeometry());
|
|
||||||
|
|
||||||
base.Render(drawingContext);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,49 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A geographic bounding box with south and north latitude and west and east longitude values in degrees.
|
/// A geographic bounding box with south and north latitude and west and east longitude values in degrees.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#if UWP || WINUI
|
#if UWP || WINUI
|
||||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||||
#else
|
#else
|
||||||
[System.ComponentModel.TypeConverter(typeof(BoundingBoxConverter))]
|
[System.ComponentModel.TypeConverter(typeof(BoundingBoxConverter))]
|
||||||
#endif
|
#endif
|
||||||
public class BoundingBox(double latitude1, double longitude1, double latitude2, double longitude2)
|
public class BoundingBox(double latitude1, double longitude1, double latitude2, double longitude2)
|
||||||
|
{
|
||||||
|
public double South { get; } = Math.Min(Math.Max(Math.Min(latitude1, latitude2), -90d), 90d);
|
||||||
|
public double North { get; } = Math.Min(Math.Max(Math.Max(latitude1, latitude2), -90d), 90d);
|
||||||
|
public double West { get; } = Math.Min(longitude1, longitude2);
|
||||||
|
public double East { get; } = Math.Max(longitude1, longitude2);
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
{
|
{
|
||||||
public double South { get; } = Math.Min(Math.Max(Math.Min(latitude1, latitude2), -90d), 90d);
|
return string.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3}", South, West, North, East);
|
||||||
public double North { get; } = Math.Min(Math.Max(Math.Max(latitude1, latitude2), -90d), 90d);
|
}
|
||||||
public double West { get; } = Math.Min(longitude1, longitude2);
|
|
||||||
public double East { get; } = Math.Max(longitude1, longitude2);
|
|
||||||
|
|
||||||
public override string ToString()
|
/// <summary>
|
||||||
|
/// Creates a BoundingBox instance from a string containing a comma-separated sequence of four floating point numbers.
|
||||||
|
/// </summary>
|
||||||
|
public static BoundingBox Parse(string boundingBox)
|
||||||
|
{
|
||||||
|
string[] values = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(boundingBox))
|
||||||
{
|
{
|
||||||
return string.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3}", South, West, North, East);
|
values = boundingBox.Split(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (values == null || values.Length != 4 && values.Length != 5)
|
||||||
/// Creates a BoundingBox instance from a string containing a comma-separated sequence of four floating point numbers.
|
|
||||||
/// </summary>
|
|
||||||
public static BoundingBox Parse(string boundingBox)
|
|
||||||
{
|
{
|
||||||
string[] values = null;
|
throw new FormatException($"{nameof(BoundingBox)} string must contain a comma-separated sequence of four floating point numbers.");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(boundingBox))
|
|
||||||
{
|
|
||||||
values = boundingBox.Split(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values == null || values.Length != 4 && values.Length != 5)
|
|
||||||
{
|
|
||||||
throw new FormatException($"{nameof(BoundingBox)} string must contain a comma-separated sequence of four floating point numbers.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BoundingBox(
|
|
||||||
double.Parse(values[0], NumberStyles.Float, CultureInfo.InvariantCulture),
|
|
||||||
double.Parse(values[1], NumberStyles.Float, CultureInfo.InvariantCulture),
|
|
||||||
double.Parse(values[2], NumberStyles.Float, CultureInfo.InvariantCulture),
|
|
||||||
double.Parse(values[3], NumberStyles.Float, CultureInfo.InvariantCulture));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new BoundingBox(
|
||||||
|
double.Parse(values[0], NumberStyles.Float, CultureInfo.InvariantCulture),
|
||||||
|
double.Parse(values[1], NumberStyles.Float, CultureInfo.InvariantCulture),
|
||||||
|
double.Parse(values[2], NumberStyles.Float, CultureInfo.InvariantCulture),
|
||||||
|
double.Parse(values[3], NumberStyles.Float, CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,45 +6,44 @@ using System.Windows.Media;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Equirectangular Projection - EPSG:4326.
|
||||||
|
/// Equidistant cylindrical projection with zero standard parallel and central meridian.
|
||||||
|
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.90-91.
|
||||||
|
/// </summary>
|
||||||
|
public class EquirectangularProjection : MapProjection
|
||||||
{
|
{
|
||||||
/// <summary>
|
public const string DefaultCrsId = "EPSG:4326";
|
||||||
/// Equirectangular Projection - EPSG:4326.
|
|
||||||
/// Equidistant cylindrical projection with zero standard parallel and central meridian.
|
public EquirectangularProjection() // parameterless constructor for XAML
|
||||||
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.90-91.
|
: this(DefaultCrsId)
|
||||||
/// </summary>
|
|
||||||
public class EquirectangularProjection : MapProjection
|
|
||||||
{
|
{
|
||||||
public const string DefaultCrsId = "EPSG:4326";
|
}
|
||||||
|
|
||||||
public EquirectangularProjection() // parameterless constructor for XAML
|
public EquirectangularProjection(string crsId)
|
||||||
: this(DefaultCrsId)
|
{
|
||||||
{
|
IsNormalCylindrical = true;
|
||||||
}
|
CrsId = crsId;
|
||||||
|
}
|
||||||
|
|
||||||
public EquirectangularProjection(string crsId)
|
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||||
{
|
{
|
||||||
IsNormalCylindrical = true;
|
return new Matrix(1d / Math.Cos(latitude * Math.PI / 180d), 0d, 0d, 1d, 0d, 0d);
|
||||||
CrsId = crsId;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
public override Point LocationToMap(double latitude, double longitude)
|
||||||
{
|
{
|
||||||
return new Matrix(1d / Math.Cos(latitude * Math.PI / 180d), 0d, 0d, 1d, 0d, 0d);
|
return new Point(
|
||||||
}
|
EquatorialRadius * Math.PI / 180d * longitude,
|
||||||
|
EquatorialRadius * Math.PI / 180d * latitude);
|
||||||
|
}
|
||||||
|
|
||||||
public override Point LocationToMap(double latitude, double longitude)
|
public override Location MapToLocation(double x, double y)
|
||||||
{
|
{
|
||||||
return new Point(
|
return new Location(
|
||||||
EquatorialRadius * Math.PI / 180d * longitude,
|
y / EquatorialRadius * 180d / Math.PI,
|
||||||
EquatorialRadius * Math.PI / 180d * latitude);
|
x / EquatorialRadius * 180d / Math.PI);
|
||||||
}
|
|
||||||
|
|
||||||
public override Location MapToLocation(double x, double y)
|
|
||||||
{
|
|
||||||
return new Location(
|
|
||||||
y / EquatorialRadius * 180d / Math.PI,
|
|
||||||
x / EquatorialRadius * 180d / Math.PI);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public static class FilePath
|
||||||
{
|
{
|
||||||
public static class FilePath
|
public static string GetFullPath(string path)
|
||||||
{
|
{
|
||||||
public static string GetFullPath(string path)
|
|
||||||
{
|
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
return Path.GetFullPath(path);
|
return Path.GetFullPath(path);
|
||||||
#else
|
#else
|
||||||
return Path.GetFullPath(path, System.AppDomain.CurrentDomain.BaseDirectory);
|
return Path.GetFullPath(path, System.AppDomain.CurrentDomain.BaseDirectory);
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,183 +29,182 @@ using Shape = Avalonia.Controls.Shapes.Shape;
|
||||||
using BitmapSource = Avalonia.Media.Imaging.Bitmap;
|
using BitmapSource = Avalonia.Media.Imaging.Bitmap;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public static partial class GeoImage
|
||||||
{
|
{
|
||||||
public static partial class GeoImage
|
private class GeoBitmap
|
||||||
{
|
{
|
||||||
private class GeoBitmap
|
public GeoBitmap(BitmapSource bitmap, Matrix transform, MapProjection projection)
|
||||||
{
|
{
|
||||||
public GeoBitmap(BitmapSource bitmap, Matrix transform, MapProjection projection)
|
var p1 = transform.Transform(new Point());
|
||||||
{
|
|
||||||
var p1 = transform.Transform(new Point());
|
|
||||||
#if AVALONIA
|
#if AVALONIA
|
||||||
var p2 = transform.Transform(new Point(bitmap.PixelSize.Width, bitmap.PixelSize.Height));
|
var p2 = transform.Transform(new Point(bitmap.PixelSize.Width, bitmap.PixelSize.Height));
|
||||||
#else
|
#else
|
||||||
var p2 = transform.Transform(new Point(bitmap.PixelWidth, bitmap.PixelHeight));
|
var p2 = transform.Transform(new Point(bitmap.PixelWidth, bitmap.PixelHeight));
|
||||||
#endif
|
#endif
|
||||||
BitmapSource = bitmap;
|
BitmapSource = bitmap;
|
||||||
|
|
||||||
if (projection != null)
|
if (projection != null)
|
||||||
{
|
|
||||||
var sw = projection.MapToLocation(p1);
|
|
||||||
var ne = projection.MapToLocation(p2);
|
|
||||||
BoundingBox = new BoundingBox(sw.Latitude, sw.Longitude, ne.Latitude, ne.Longitude);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
BoundingBox = new BoundingBox(p1.Y, p1.X, p2.Y, p2.X);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public BitmapSource BitmapSource { get; }
|
|
||||||
public BoundingBox BoundingBox { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private const ushort ProjectedCRSGeoKey = 3072;
|
|
||||||
private const ushort GeoKeyDirectoryTag = 34735;
|
|
||||||
private const ushort ModelPixelScaleTag = 33550;
|
|
||||||
private const ushort ModelTiePointTag = 33922;
|
|
||||||
private const ushort ModelTransformationTag = 34264;
|
|
||||||
private const ushort NoDataTag = 42113;
|
|
||||||
|
|
||||||
private static string QueryString(ushort tag) => $"/ifd/{{ushort={tag}}}";
|
|
||||||
|
|
||||||
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(GeoImage));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty SourcePathProperty =
|
|
||||||
DependencyPropertyHelper.RegisterAttached<string>("SourcePath", typeof(GeoImage), null,
|
|
||||||
async (element, oldValue, newValue) => await LoadGeoImage(element, newValue));
|
|
||||||
|
|
||||||
public static string GetSourcePath(FrameworkElement image)
|
|
||||||
{
|
|
||||||
return (string)image.GetValue(SourcePathProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SetSourcePath(FrameworkElement image, string value)
|
|
||||||
{
|
|
||||||
image.SetValue(SourcePathProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<Image> CreateAsync(string sourcePath)
|
|
||||||
{
|
|
||||||
var image = new Image();
|
|
||||||
|
|
||||||
await LoadGeoImage(image, sourcePath);
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task LoadGeoImageAsync(this Image image, string sourcePath)
|
|
||||||
{
|
|
||||||
return LoadGeoImage(image, sourcePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task LoadGeoImageAsync(this Shape shape, string sourcePath)
|
|
||||||
{
|
|
||||||
return LoadGeoImage(shape, sourcePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task LoadGeoImage(FrameworkElement element, string sourcePath)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(sourcePath))
|
|
||||||
{
|
{
|
||||||
try
|
var sw = projection.MapToLocation(p1);
|
||||||
{
|
var ne = projection.MapToLocation(p2);
|
||||||
var geoBitmap = await LoadGeoBitmap(sourcePath);
|
BoundingBox = new BoundingBox(sw.Latitude, sw.Longitude, ne.Latitude, ne.Longitude);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BoundingBox = new BoundingBox(p1.Y, p1.X, p2.Y, p2.X);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (element is Image image)
|
public BitmapSource BitmapSource { get; }
|
||||||
|
public BoundingBox BoundingBox { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private const ushort ProjectedCRSGeoKey = 3072;
|
||||||
|
private const ushort GeoKeyDirectoryTag = 34735;
|
||||||
|
private const ushort ModelPixelScaleTag = 33550;
|
||||||
|
private const ushort ModelTiePointTag = 33922;
|
||||||
|
private const ushort ModelTransformationTag = 34264;
|
||||||
|
private const ushort NoDataTag = 42113;
|
||||||
|
|
||||||
|
private static string QueryString(ushort tag) => $"/ifd/{{ushort={tag}}}";
|
||||||
|
|
||||||
|
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(GeoImage));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty SourcePathProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<string>("SourcePath", typeof(GeoImage), null,
|
||||||
|
async (element, oldValue, newValue) => await LoadGeoImage(element, newValue));
|
||||||
|
|
||||||
|
public static string GetSourcePath(FrameworkElement image)
|
||||||
|
{
|
||||||
|
return (string)image.GetValue(SourcePathProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetSourcePath(FrameworkElement image, string value)
|
||||||
|
{
|
||||||
|
image.SetValue(SourcePathProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<Image> CreateAsync(string sourcePath)
|
||||||
|
{
|
||||||
|
var image = new Image();
|
||||||
|
|
||||||
|
await LoadGeoImage(image, sourcePath);
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task LoadGeoImageAsync(this Image image, string sourcePath)
|
||||||
|
{
|
||||||
|
return LoadGeoImage(image, sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task LoadGeoImageAsync(this Shape shape, string sourcePath)
|
||||||
|
{
|
||||||
|
return LoadGeoImage(shape, sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task LoadGeoImage(FrameworkElement element, string sourcePath)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(sourcePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var geoBitmap = await LoadGeoBitmap(sourcePath);
|
||||||
|
|
||||||
|
if (element is Image image)
|
||||||
|
{
|
||||||
|
image.Stretch = Stretch.Fill;
|
||||||
|
image.Source = geoBitmap.BitmapSource;
|
||||||
|
}
|
||||||
|
else if (element is Shape shape)
|
||||||
|
{
|
||||||
|
shape.Stretch = Stretch.Fill;
|
||||||
|
shape.Fill = new ImageBrush
|
||||||
{
|
{
|
||||||
image.Stretch = Stretch.Fill;
|
Stretch = Stretch.Fill,
|
||||||
image.Source = geoBitmap.BitmapSource;
|
|
||||||
}
|
|
||||||
else if (element is Shape shape)
|
|
||||||
{
|
|
||||||
shape.Stretch = Stretch.Fill;
|
|
||||||
shape.Fill = new ImageBrush
|
|
||||||
{
|
|
||||||
Stretch = Stretch.Fill,
|
|
||||||
#if AVALONIA
|
#if AVALONIA
|
||||||
Source = geoBitmap.BitmapSource
|
Source = geoBitmap.BitmapSource
|
||||||
#else
|
#else
|
||||||
ImageSource = geoBitmap.BitmapSource
|
ImageSource = geoBitmap.BitmapSource
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
MapPanel.SetBoundingBox(element, geoBitmap.BoundingBox);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger?.LogError(ex, "Failed loading from {path}", sourcePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MapPanel.SetBoundingBox(element, geoBitmap.BoundingBox);
|
||||||
}
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
|
|
||||||
private static async Task<GeoBitmap> LoadGeoBitmap(string sourcePath)
|
|
||||||
{
|
|
||||||
var ext = System.IO.Path.GetExtension(sourcePath);
|
|
||||||
|
|
||||||
if (ext.Length >= 4)
|
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(sourcePath);
|
Logger?.LogError(ex, "Failed loading from {path}", sourcePath);
|
||||||
var file = Path.GetFileNameWithoutExtension(sourcePath);
|
|
||||||
var worldFilePath = Path.Combine(dir, file + ext.Remove(2, 1) + "w");
|
|
||||||
|
|
||||||
if (File.Exists(worldFilePath))
|
|
||||||
{
|
|
||||||
return new GeoBitmap(
|
|
||||||
(BitmapSource)await ImageLoader.LoadImageAsync(sourcePath),
|
|
||||||
await ReadWorldFileMatrix(worldFilePath),
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await LoadGeoTiff(sourcePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<Matrix> ReadWorldFileMatrix(string worldFilePath)
|
|
||||||
{
|
|
||||||
using var fileStream = File.OpenRead(worldFilePath);
|
|
||||||
using var streamReader = new StreamReader(fileStream);
|
|
||||||
|
|
||||||
var parameters = new double[6];
|
|
||||||
var index = 0;
|
|
||||||
string line;
|
|
||||||
|
|
||||||
while (index < 6 &&
|
|
||||||
(line = await streamReader.ReadLineAsync()) != null &&
|
|
||||||
double.TryParse(line, NumberStyles.Float, CultureInfo.InvariantCulture, out double parameter))
|
|
||||||
{
|
|
||||||
parameters[index++] = parameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index != 6)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Insufficient number of parameters in world file {worldFilePath}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Matrix( // https://en.wikipedia.org/wiki/World_file
|
|
||||||
parameters[0], // line 1: A
|
|
||||||
parameters[1], // line 2: D
|
|
||||||
parameters[2], // line 3: B
|
|
||||||
parameters[3], // line 4: E
|
|
||||||
parameters[4], // line 5: C
|
|
||||||
parameters[5]); // line 6: F
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MapProjection GetProjection(short[] geoKeyDirectory)
|
|
||||||
{
|
|
||||||
for (var i = 4; i < geoKeyDirectory.Length - 3; i += 4)
|
|
||||||
{
|
|
||||||
if (geoKeyDirectory[i] == ProjectedCRSGeoKey && geoKeyDirectory[i + 1] == 0)
|
|
||||||
{
|
|
||||||
var epsgCode = geoKeyDirectory[i + 3];
|
|
||||||
|
|
||||||
return MapProjection.Parse($"EPSG:{epsgCode}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<GeoBitmap> LoadGeoBitmap(string sourcePath)
|
||||||
|
{
|
||||||
|
var ext = System.IO.Path.GetExtension(sourcePath);
|
||||||
|
|
||||||
|
if (ext.Length >= 4)
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(sourcePath);
|
||||||
|
var file = Path.GetFileNameWithoutExtension(sourcePath);
|
||||||
|
var worldFilePath = Path.Combine(dir, file + ext.Remove(2, 1) + "w");
|
||||||
|
|
||||||
|
if (File.Exists(worldFilePath))
|
||||||
|
{
|
||||||
|
return new GeoBitmap(
|
||||||
|
(BitmapSource)await ImageLoader.LoadImageAsync(sourcePath),
|
||||||
|
await ReadWorldFileMatrix(worldFilePath),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await LoadGeoTiff(sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Matrix> ReadWorldFileMatrix(string worldFilePath)
|
||||||
|
{
|
||||||
|
using var fileStream = File.OpenRead(worldFilePath);
|
||||||
|
using var streamReader = new StreamReader(fileStream);
|
||||||
|
|
||||||
|
var parameters = new double[6];
|
||||||
|
var index = 0;
|
||||||
|
string line;
|
||||||
|
|
||||||
|
while (index < 6 &&
|
||||||
|
(line = await streamReader.ReadLineAsync()) != null &&
|
||||||
|
double.TryParse(line, NumberStyles.Float, CultureInfo.InvariantCulture, out double parameter))
|
||||||
|
{
|
||||||
|
parameters[index++] = parameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index != 6)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Insufficient number of parameters in world file {worldFilePath}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Matrix( // https://en.wikipedia.org/wiki/World_file
|
||||||
|
parameters[0], // line 1: A
|
||||||
|
parameters[1], // line 2: D
|
||||||
|
parameters[2], // line 3: B
|
||||||
|
parameters[3], // line 4: E
|
||||||
|
parameters[4], // line 5: C
|
||||||
|
parameters[5]); // line 6: F
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MapProjection GetProjection(short[] geoKeyDirectory)
|
||||||
|
{
|
||||||
|
for (var i = 4; i < geoKeyDirectory.Length - 3; i += 4)
|
||||||
|
{
|
||||||
|
if (geoKeyDirectory[i] == ProjectedCRSGeoKey && geoKeyDirectory[i + 1] == 0)
|
||||||
|
{
|
||||||
|
var epsgCode = geoKeyDirectory[i + 3];
|
||||||
|
|
||||||
|
return MapProjection.Parse($"EPSG:{epsgCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,213 +24,212 @@ using Avalonia.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class GroundOverlay : MapPanel
|
||||||
{
|
{
|
||||||
public partial class GroundOverlay : MapPanel
|
private class ImageOverlay
|
||||||
{
|
{
|
||||||
private class ImageOverlay
|
public ImageOverlay(string path, BoundingBox latLonBox, int zIndex)
|
||||||
{
|
{
|
||||||
public ImageOverlay(string path, BoundingBox latLonBox, int zIndex)
|
ImagePath = path;
|
||||||
|
SetBoundingBox(Image, latLonBox);
|
||||||
|
Image.SetValue(Canvas.ZIndexProperty, zIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ImagePath { get; }
|
||||||
|
|
||||||
|
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
|
||||||
|
|
||||||
|
public async Task LoadImage(Uri docUri)
|
||||||
|
{
|
||||||
|
Image.Source = await ImageLoader.LoadImageAsync(new Uri(docUri, ImagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadImage(ZipArchive archive)
|
||||||
|
{
|
||||||
|
var entry = archive.GetEntry(ImagePath);
|
||||||
|
|
||||||
|
if (entry != null)
|
||||||
{
|
{
|
||||||
ImagePath = path;
|
using var memoryStream = new MemoryStream((int)entry.Length);
|
||||||
SetBoundingBox(Image, latLonBox);
|
|
||||||
Image.SetValue(Canvas.ZIndexProperty, zIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ImagePath { get; }
|
using (var zipStream = entry.Open())
|
||||||
|
|
||||||
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
|
|
||||||
|
|
||||||
public async Task LoadImage(Uri docUri)
|
|
||||||
{
|
|
||||||
Image.Source = await ImageLoader.LoadImageAsync(new Uri(docUri, ImagePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoadImage(ZipArchive archive)
|
|
||||||
{
|
|
||||||
var entry = archive.GetEntry(ImagePath);
|
|
||||||
|
|
||||||
if (entry != null)
|
|
||||||
{
|
{
|
||||||
using var memoryStream = new MemoryStream((int)entry.Length);
|
zipStream.CopyTo(memoryStream); // can't use CopyToAsync with ZipArchive
|
||||||
|
|
||||||
using (var zipStream = entry.Open())
|
|
||||||
{
|
|
||||||
zipStream.CopyTo(memoryStream); // can't use CopyToAsync with ZipArchive
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
Image.Source = await ImageLoader.LoadImageAsync(memoryStream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
Image.Source = await ImageLoader.LoadImageAsync(memoryStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(GroundOverlay));
|
|
||||||
|
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(GroundOverlay));
|
||||||
public static readonly DependencyProperty SourcePathProperty =
|
|
||||||
DependencyPropertyHelper.Register<GroundOverlay, string>(nameof(SourcePath), null,
|
public static readonly DependencyProperty SourcePathProperty =
|
||||||
async (groundOverlay, oldValue, newValue) => await groundOverlay.LoadAsync(newValue));
|
DependencyPropertyHelper.Register<GroundOverlay, string>(nameof(SourcePath), null,
|
||||||
|
async (groundOverlay, oldValue, newValue) => await groundOverlay.LoadAsync(newValue));
|
||||||
public string SourcePath
|
|
||||||
{
|
public string SourcePath
|
||||||
get => (string)GetValue(SourcePathProperty);
|
{
|
||||||
set => SetValue(SourcePathProperty, value);
|
get => (string)GetValue(SourcePathProperty);
|
||||||
}
|
set => SetValue(SourcePathProperty, value);
|
||||||
|
}
|
||||||
public static async Task<GroundOverlay> CreateAsync(string sourcePath)
|
|
||||||
{
|
public static async Task<GroundOverlay> CreateAsync(string sourcePath)
|
||||||
var groundOverlay = new GroundOverlay();
|
{
|
||||||
|
var groundOverlay = new GroundOverlay();
|
||||||
await groundOverlay.LoadAsync(sourcePath);
|
|
||||||
|
await groundOverlay.LoadAsync(sourcePath);
|
||||||
return groundOverlay;
|
|
||||||
}
|
return groundOverlay;
|
||||||
|
}
|
||||||
public async Task LoadAsync(string sourcePath)
|
|
||||||
{
|
public async Task LoadAsync(string sourcePath)
|
||||||
List<ImageOverlay> imageOverlays = null;
|
{
|
||||||
|
List<ImageOverlay> imageOverlays = null;
|
||||||
if (!string.IsNullOrEmpty(sourcePath))
|
|
||||||
{
|
if (!string.IsNullOrEmpty(sourcePath))
|
||||||
try
|
{
|
||||||
{
|
try
|
||||||
var ext = Path.GetExtension(sourcePath).ToLower();
|
{
|
||||||
|
var ext = Path.GetExtension(sourcePath).ToLower();
|
||||||
if (ext == ".kmz")
|
|
||||||
{
|
if (ext == ".kmz")
|
||||||
imageOverlays = await LoadImageOverlaysFromArchive(sourcePath);
|
{
|
||||||
}
|
imageOverlays = await LoadImageOverlaysFromArchive(sourcePath);
|
||||||
else if (ext == ".kml")
|
}
|
||||||
{
|
else if (ext == ".kml")
|
||||||
imageOverlays = await LoadImageOverlaysFromFile(sourcePath);
|
{
|
||||||
}
|
imageOverlays = await LoadImageOverlaysFromFile(sourcePath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
Logger?.LogError(ex, "Failed loading from {path}", sourcePath);
|
{
|
||||||
}
|
Logger?.LogError(ex, "Failed loading from {path}", sourcePath);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Children.Clear();
|
|
||||||
|
Children.Clear();
|
||||||
if (imageOverlays != null)
|
|
||||||
{
|
if (imageOverlays != null)
|
||||||
foreach (var imageOverlay in imageOverlays)
|
{
|
||||||
{
|
foreach (var imageOverlay in imageOverlays)
|
||||||
Children.Add(imageOverlay.Image);
|
{
|
||||||
}
|
Children.Add(imageOverlay.Image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private static async Task<List<ImageOverlay>> LoadImageOverlaysFromArchive(string archiveFilePath)
|
|
||||||
{
|
private static async Task<List<ImageOverlay>> LoadImageOverlaysFromArchive(string archiveFilePath)
|
||||||
using var archive = ZipFile.OpenRead(archiveFilePath);
|
{
|
||||||
|
using var archive = ZipFile.OpenRead(archiveFilePath);
|
||||||
var docEntry = archive.GetEntry("doc.kml") ??
|
|
||||||
archive.Entries.FirstOrDefault(e => e.Name.EndsWith(".kml")) ??
|
var docEntry = archive.GetEntry("doc.kml") ??
|
||||||
throw new ArgumentException($"No KML entry found in {archiveFilePath}.");
|
archive.Entries.FirstOrDefault(e => e.Name.EndsWith(".kml")) ??
|
||||||
XElement element;
|
throw new ArgumentException($"No KML entry found in {archiveFilePath}.");
|
||||||
|
XElement element;
|
||||||
using (var stream = docEntry.Open())
|
|
||||||
{
|
using (var stream = docEntry.Open())
|
||||||
element = await XDocument.LoadRootElementAsync(stream);
|
{
|
||||||
}
|
element = await XDocument.LoadRootElementAsync(stream);
|
||||||
|
}
|
||||||
return await LoadImageOverlays(element, imageOverlay => imageOverlay.LoadImage(archive));
|
|
||||||
}
|
return await LoadImageOverlays(element, imageOverlay => imageOverlay.LoadImage(archive));
|
||||||
|
}
|
||||||
private static async Task<List<ImageOverlay>> LoadImageOverlaysFromFile(string docFilePath)
|
|
||||||
{
|
private static async Task<List<ImageOverlay>> LoadImageOverlaysFromFile(string docFilePath)
|
||||||
var docUri = new Uri(FilePath.GetFullPath(docFilePath));
|
{
|
||||||
XElement element;
|
var docUri = new Uri(FilePath.GetFullPath(docFilePath));
|
||||||
|
XElement element;
|
||||||
using (var stream = File.OpenRead(docUri.AbsolutePath))
|
|
||||||
{
|
using (var stream = File.OpenRead(docUri.AbsolutePath))
|
||||||
element = await XDocument.LoadRootElementAsync(stream);
|
{
|
||||||
}
|
element = await XDocument.LoadRootElementAsync(stream);
|
||||||
|
}
|
||||||
return await LoadImageOverlays(element, imageOverlay => imageOverlay.LoadImage(docUri));
|
|
||||||
}
|
return await LoadImageOverlays(element, imageOverlay => imageOverlay.LoadImage(docUri));
|
||||||
|
}
|
||||||
private static async Task<List<ImageOverlay>> LoadImageOverlays(XElement rootElement, Func<ImageOverlay, Task> loadFunc)
|
|
||||||
{
|
private static async Task<List<ImageOverlay>> LoadImageOverlays(XElement rootElement, Func<ImageOverlay, Task> loadFunc)
|
||||||
var imageOverlays = ReadImageOverlays(rootElement);
|
{
|
||||||
|
var imageOverlays = ReadImageOverlays(rootElement);
|
||||||
await Task.WhenAll(imageOverlays.Select(loadFunc));
|
|
||||||
|
await Task.WhenAll(imageOverlays.Select(loadFunc));
|
||||||
return imageOverlays;
|
|
||||||
}
|
return imageOverlays;
|
||||||
|
}
|
||||||
private static List<ImageOverlay> ReadImageOverlays(XElement rootElement)
|
|
||||||
{
|
private static List<ImageOverlay> ReadImageOverlays(XElement rootElement)
|
||||||
var ns = rootElement.Name.Namespace;
|
{
|
||||||
var docElement = rootElement.Element(ns + "Document") ?? rootElement;
|
var ns = rootElement.Name.Namespace;
|
||||||
var imageOverlays = new List<ImageOverlay>();
|
var docElement = rootElement.Element(ns + "Document") ?? rootElement;
|
||||||
|
var imageOverlays = new List<ImageOverlay>();
|
||||||
foreach (var folderElement in docElement.Elements(ns + "Folder"))
|
|
||||||
{
|
foreach (var folderElement in docElement.Elements(ns + "Folder"))
|
||||||
foreach (var groundOverlayElement in folderElement.Elements(ns + "GroundOverlay"))
|
{
|
||||||
{
|
foreach (var groundOverlayElement in folderElement.Elements(ns + "GroundOverlay"))
|
||||||
var pathElement = groundOverlayElement.Element(ns + "Icon");
|
{
|
||||||
var path = pathElement?.Element(ns + "href")?.Value;
|
var pathElement = groundOverlayElement.Element(ns + "Icon");
|
||||||
|
var path = pathElement?.Element(ns + "href")?.Value;
|
||||||
var latLonBoxElement = groundOverlayElement.Element(ns + "LatLonBox");
|
|
||||||
var latLonBox = latLonBoxElement != null ? ReadLatLonBox(latLonBoxElement) : null;
|
var latLonBoxElement = groundOverlayElement.Element(ns + "LatLonBox");
|
||||||
|
var latLonBox = latLonBoxElement != null ? ReadLatLonBox(latLonBoxElement) : null;
|
||||||
var drawOrder = groundOverlayElement.Element(ns + "drawOrder")?.Value;
|
|
||||||
var zIndex = drawOrder != null ? int.Parse(drawOrder) : 0;
|
var drawOrder = groundOverlayElement.Element(ns + "drawOrder")?.Value;
|
||||||
|
var zIndex = drawOrder != null ? int.Parse(drawOrder) : 0;
|
||||||
if (latLonBox != null && path != null)
|
|
||||||
{
|
if (latLonBox != null && path != null)
|
||||||
imageOverlays.Add(new ImageOverlay(path, latLonBox, zIndex));
|
{
|
||||||
}
|
imageOverlays.Add(new ImageOverlay(path, latLonBox, zIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return imageOverlays;
|
|
||||||
}
|
return imageOverlays;
|
||||||
|
}
|
||||||
private static BoundingBox ReadLatLonBox(XElement latLonBoxElement)
|
|
||||||
{
|
private static BoundingBox ReadLatLonBox(XElement latLonBoxElement)
|
||||||
var ns = latLonBoxElement.Name.Namespace;
|
{
|
||||||
var north = double.NaN;
|
var ns = latLonBoxElement.Name.Namespace;
|
||||||
var south = double.NaN;
|
var north = double.NaN;
|
||||||
var east = double.NaN;
|
var south = double.NaN;
|
||||||
var west = double.NaN;
|
var east = double.NaN;
|
||||||
|
var west = double.NaN;
|
||||||
var value = latLonBoxElement.Element(ns + "north")?.Value;
|
|
||||||
if (value != null)
|
var value = latLonBoxElement.Element(ns + "north")?.Value;
|
||||||
{
|
if (value != null)
|
||||||
north = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
{
|
||||||
}
|
north = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
value = latLonBoxElement.Element(ns + "south")?.Value;
|
|
||||||
if (value != null)
|
value = latLonBoxElement.Element(ns + "south")?.Value;
|
||||||
{
|
if (value != null)
|
||||||
south = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
{
|
||||||
}
|
south = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
value = latLonBoxElement.Element(ns + "east")?.Value;
|
|
||||||
if (value != null)
|
value = latLonBoxElement.Element(ns + "east")?.Value;
|
||||||
{
|
if (value != null)
|
||||||
east = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
{
|
||||||
}
|
east = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
value = latLonBoxElement.Element(ns + "west")?.Value;
|
|
||||||
if (value != null)
|
value = latLonBoxElement.Element(ns + "west")?.Value;
|
||||||
{
|
if (value != null)
|
||||||
west = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
{
|
||||||
}
|
west = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
if (double.IsNaN(north) || double.IsNaN(south) ||
|
|
||||||
double.IsNaN(east) || double.IsNaN(west) ||
|
if (double.IsNaN(north) || double.IsNaN(south) ||
|
||||||
north <= south || east <= west)
|
double.IsNaN(east) || double.IsNaN(west) ||
|
||||||
{
|
north <= south || east <= west)
|
||||||
throw new FormatException("Invalid LatLonBox");
|
{
|
||||||
}
|
throw new FormatException("Invalid LatLonBox");
|
||||||
|
}
|
||||||
return new BoundingBox(south, west, north, east);
|
|
||||||
}
|
return new BoundingBox(south, west, north, east);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,356 +8,355 @@ using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MapControl.Caching
|
namespace MapControl.Caching;
|
||||||
|
|
||||||
|
public class ImageFileCacheOptions : IOptions<ImageFileCacheOptions>
|
||||||
{
|
{
|
||||||
public class ImageFileCacheOptions : IOptions<ImageFileCacheOptions>
|
public ImageFileCacheOptions Value => this;
|
||||||
|
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromHours(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IDistributedCache implementation that creates a single file per cache entry.
|
||||||
|
/// The cache expiration time is stored in the file's CreationTime property.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class ImageFileCache : IDistributedCache, IDisposable
|
||||||
|
{
|
||||||
|
private readonly MemoryDistributedCache memoryCache;
|
||||||
|
private readonly DirectoryInfo rootDirectory;
|
||||||
|
private readonly Timer expirationScanTimer;
|
||||||
|
private readonly ILogger logger;
|
||||||
|
private bool scanningExpiration;
|
||||||
|
|
||||||
|
public ImageFileCache(string path, ILoggerFactory loggerFactory = null)
|
||||||
|
: this(new ImageFileCacheOptions { Path = path }, loggerFactory)
|
||||||
{
|
{
|
||||||
public ImageFileCacheOptions Value => this;
|
|
||||||
|
|
||||||
public string Path { get; set; }
|
|
||||||
|
|
||||||
public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromHours(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public ImageFileCache(IOptions<ImageFileCacheOptions> optionsAccessor, ILoggerFactory loggerFactory = null)
|
||||||
/// IDistributedCache implementation that creates a single file per cache entry.
|
: this(optionsAccessor.Value, loggerFactory)
|
||||||
/// The cache expiration time is stored in the file's CreationTime property.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class ImageFileCache : IDistributedCache, IDisposable
|
|
||||||
{
|
{
|
||||||
private readonly MemoryDistributedCache memoryCache;
|
}
|
||||||
private readonly DirectoryInfo rootDirectory;
|
|
||||||
private readonly Timer expirationScanTimer;
|
|
||||||
private readonly ILogger logger;
|
|
||||||
private bool scanningExpiration;
|
|
||||||
|
|
||||||
public ImageFileCache(string path, ILoggerFactory loggerFactory = null)
|
public ImageFileCache(ImageFileCacheOptions options, ILoggerFactory loggerFactory = null)
|
||||||
: this(new ImageFileCacheOptions { Path = path }, loggerFactory)
|
{
|
||||||
|
var path = options.Path;
|
||||||
|
|
||||||
|
rootDirectory = new DirectoryInfo(!string.IsNullOrEmpty(path) ? path : "TileCache");
|
||||||
|
rootDirectory.Create();
|
||||||
|
|
||||||
|
logger = loggerFactory?.CreateLogger(typeof(ImageFileCache));
|
||||||
|
logger?.LogInformation("Started in {name}", rootDirectory.FullName);
|
||||||
|
|
||||||
|
var memoryCacheOptions = new MemoryDistributedCacheOptions();
|
||||||
|
|
||||||
|
if (options.ExpirationScanFrequency > TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
|
memoryCacheOptions.ExpirationScanFrequency = options.ExpirationScanFrequency;
|
||||||
|
|
||||||
|
expirationScanTimer = new Timer(_ => DeleteExpiredItems(), null, TimeSpan.Zero, options.ExpirationScanFrequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageFileCache(IOptions<ImageFileCacheOptions> optionsAccessor, ILoggerFactory loggerFactory = null)
|
memoryCache = new MemoryDistributedCache(Options.Create(memoryCacheOptions));
|
||||||
: this(optionsAccessor.Value, loggerFactory)
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
expirationScanTimer?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] Get(string key)
|
||||||
|
{
|
||||||
|
byte[] value = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(key))
|
||||||
{
|
{
|
||||||
}
|
value = memoryCache.Get(key);
|
||||||
|
|
||||||
public ImageFileCache(ImageFileCacheOptions options, ILoggerFactory loggerFactory = null)
|
if (value == null)
|
||||||
{
|
|
||||||
var path = options.Path;
|
|
||||||
|
|
||||||
rootDirectory = new DirectoryInfo(!string.IsNullOrEmpty(path) ? path : "TileCache");
|
|
||||||
rootDirectory.Create();
|
|
||||||
|
|
||||||
logger = loggerFactory?.CreateLogger(typeof(ImageFileCache));
|
|
||||||
logger?.LogInformation("Started in {name}", rootDirectory.FullName);
|
|
||||||
|
|
||||||
var memoryCacheOptions = new MemoryDistributedCacheOptions();
|
|
||||||
|
|
||||||
if (options.ExpirationScanFrequency > TimeSpan.Zero)
|
|
||||||
{
|
{
|
||||||
memoryCacheOptions.ExpirationScanFrequency = options.ExpirationScanFrequency;
|
|
||||||
|
|
||||||
expirationScanTimer = new Timer(_ => DeleteExpiredItems(), null, TimeSpan.Zero, options.ExpirationScanFrequency);
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryCache = new MemoryDistributedCache(Options.Create(memoryCacheOptions));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
expirationScanTimer?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] Get(string key)
|
|
||||||
{
|
|
||||||
byte[] value = null;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(key))
|
|
||||||
{
|
|
||||||
value = memoryCache.Get(key);
|
|
||||||
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
var file = GetFile(key);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (file != null && file.Exists && file.CreationTime > DateTime.Now)
|
|
||||||
{
|
|
||||||
value = ReadAllBytes(file);
|
|
||||||
|
|
||||||
var options = new DistributedCacheEntryOptions { AbsoluteExpiration = file.CreationTime };
|
|
||||||
|
|
||||||
memoryCache.Set(key, value, options);
|
|
||||||
|
|
||||||
logger?.LogDebug("Read {name}", file.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger?.LogError(ex, "Failed reading {name}", file.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<byte[]> GetAsync(string key, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
byte[] value = null;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(key))
|
|
||||||
{
|
|
||||||
value = await memoryCache.GetAsync(key, token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
var file = GetFile(key);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (file != null && file.Exists && file.CreationTime > DateTime.Now && !token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
value = await ReadAllBytes(file, token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var options = new DistributedCacheEntryOptions { AbsoluteExpiration = file.CreationTime };
|
|
||||||
|
|
||||||
await memoryCache.SetAsync(key, value, options, token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
logger?.LogDebug("Read {name}", file.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger?.LogError(ex, "Failed reading {name}", file.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(key) && value != null && options != null)
|
|
||||||
{
|
|
||||||
memoryCache.Set(key, value, options);
|
|
||||||
|
|
||||||
var file = GetFile(key);
|
var file = GetFile(key);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (file != null && value?.Length > 0)
|
if (file != null && file.Exists && file.CreationTime > DateTime.Now)
|
||||||
{
|
{
|
||||||
file.Directory.Create();
|
value = ReadAllBytes(file);
|
||||||
|
|
||||||
using (var stream = file.Create())
|
var options = new DistributedCacheEntryOptions { AbsoluteExpiration = file.CreationTime };
|
||||||
{
|
|
||||||
stream.Write(value, 0, value.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetExpiration(file, options);
|
memoryCache.Set(key, value, options);
|
||||||
|
|
||||||
logger?.LogDebug("Wrote {name}", file.FullName);
|
logger?.LogDebug("Read {name}", file.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger?.LogError(ex, "Failed writing {name}", file.FullName);
|
logger?.LogError(ex, "Failed reading {name}", file.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
|
return value;
|
||||||
{
|
}
|
||||||
if (!string.IsNullOrEmpty(key) && value != null && options != null)
|
|
||||||
{
|
|
||||||
await memoryCache.SetAsync(key, value, options, token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
|
public async Task<byte[]> GetAsync(string key, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
byte[] value = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
value = await memoryCache.GetAsync(key, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
var file = GetFile(key);
|
var file = GetFile(key);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (file != null && value?.Length > 0 && !token.IsCancellationRequested)
|
if (file != null && file.Exists && file.CreationTime > DateTime.Now && !token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
file.Directory.Create();
|
value = await ReadAllBytes(file, token).ConfigureAwait(false);
|
||||||
|
|
||||||
using (var stream = file.Create())
|
var options = new DistributedCacheEntryOptions { AbsoluteExpiration = file.CreationTime };
|
||||||
{
|
|
||||||
await stream.WriteAsync(value, 0, value.Length, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetExpiration(file, options);
|
await memoryCache.SetAsync(key, value, options, token).ConfigureAwait(false);
|
||||||
|
|
||||||
logger?.LogDebug("Wrote {name}", file.FullName);
|
logger?.LogDebug("Read {name}", file.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger?.LogError(ex, "Failed writing {name}", file.FullName);
|
logger?.LogError(ex, "Failed reading {name}", file.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Refresh(string key)
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(key) && value != null && options != null)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(key))
|
memoryCache.Set(key, value, options);
|
||||||
{
|
|
||||||
memoryCache.Refresh(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RefreshAsync(string key, CancellationToken token = default)
|
var file = GetFile(key);
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(key))
|
|
||||||
{
|
|
||||||
await memoryCache.RefreshAsync(key, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Remove(string key)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(key))
|
|
||||||
{
|
|
||||||
memoryCache.Remove(key);
|
|
||||||
|
|
||||||
var file = GetFile(key);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (file != null && file.Exists)
|
|
||||||
{
|
|
||||||
file.Delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger?.LogError(ex, "Failed deleting {name}", file.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemoveAsync(string key, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(key))
|
|
||||||
{
|
|
||||||
await memoryCache.RemoveAsync(key, token);
|
|
||||||
|
|
||||||
var file = GetFile(key);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (file != null && file.Exists && !token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
file.Delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger?.LogError(ex, "Failed deleting {name}", file.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteExpiredItems()
|
|
||||||
{
|
|
||||||
if (!scanningExpiration)
|
|
||||||
{
|
|
||||||
scanningExpiration = true;
|
|
||||||
|
|
||||||
foreach (var directory in rootDirectory.EnumerateDirectories())
|
|
||||||
{
|
|
||||||
var deletedFileCount = ScanDirectory(directory);
|
|
||||||
|
|
||||||
if (deletedFileCount > 0)
|
|
||||||
{
|
|
||||||
logger?.LogInformation("Deleted {count} expired items in {name}", deletedFileCount, directory.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scanningExpiration = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int ScanDirectory(DirectoryInfo directory)
|
|
||||||
{
|
|
||||||
var deletedFileCount = 0;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
deletedFileCount = directory.EnumerateDirectories().Sum(ScanDirectory);
|
if (file != null && value?.Length > 0)
|
||||||
|
{
|
||||||
|
file.Directory.Create();
|
||||||
|
|
||||||
foreach (var file in directory.EnumerateFiles()
|
using (var stream = file.Create())
|
||||||
.Where(file => file.CreationTime > file.LastWriteTime &&
|
{
|
||||||
file.CreationTime <= DateTime.Now))
|
stream.Write(value, 0, value.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetExpiration(file, options);
|
||||||
|
|
||||||
|
logger?.LogDebug("Wrote {name}", file.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger?.LogError(ex, "Failed writing {name}", file.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(key) && value != null && options != null)
|
||||||
|
{
|
||||||
|
await memoryCache.SetAsync(key, value, options, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var file = GetFile(key);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (file != null && value?.Length > 0 && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
file.Directory.Create();
|
||||||
|
|
||||||
|
using (var stream = file.Create())
|
||||||
|
{
|
||||||
|
await stream.WriteAsync(value, 0, value.Length, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetExpiration(file, options);
|
||||||
|
|
||||||
|
logger?.LogDebug("Wrote {name}", file.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger?.LogError(ex, "Failed writing {name}", file.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh(string key)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
memoryCache.Refresh(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshAsync(string key, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
await memoryCache.RefreshAsync(key, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string key)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
memoryCache.Remove(key);
|
||||||
|
|
||||||
|
var file = GetFile(key);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (file != null && file.Exists)
|
||||||
{
|
{
|
||||||
file.Delete();
|
file.Delete();
|
||||||
deletedFileCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!directory.EnumerateFileSystemInfos().Any())
|
|
||||||
{
|
|
||||||
directory.Delete();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger?.LogError(ex, "Failed cleaning {name}", directory.FullName);
|
logger?.LogError(ex, "Failed deleting {name}", file.FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return deletedFileCount;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private FileInfo GetFile(string key)
|
public async Task RemoveAsync(string key, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(key))
|
||||||
{
|
{
|
||||||
FileInfo file = null;
|
await memoryCache.RemoveAsync(key, token);
|
||||||
|
|
||||||
|
var file = GetFile(key);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
file = new FileInfo(Path.Combine(rootDirectory.FullName, Path.Combine(key.Split('/'))));
|
if (file != null && file.Exists && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
file.Delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger?.LogError(ex, "Invalid key {key}", key);
|
logger?.LogError(ex, "Failed deleting {name}", file.FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] ReadAllBytes(FileInfo file)
|
|
||||||
{
|
|
||||||
using var stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
||||||
var buffer = new byte[stream.Length];
|
|
||||||
var offset = 0;
|
|
||||||
|
|
||||||
while (offset < buffer.Length)
|
|
||||||
{
|
|
||||||
offset += stream.Read(buffer, offset, buffer.Length - offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<byte[]> ReadAllBytes(FileInfo file, CancellationToken token)
|
|
||||||
{
|
|
||||||
using var stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
||||||
var buffer = new byte[stream.Length];
|
|
||||||
var offset = 0;
|
|
||||||
|
|
||||||
while (offset < buffer.Length)
|
|
||||||
{
|
|
||||||
offset += await stream.ReadAsync(buffer, offset, buffer.Length - offset, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SetExpiration(FileInfo file, DistributedCacheEntryOptions options)
|
|
||||||
{
|
|
||||||
file.CreationTime = options.AbsoluteExpiration.HasValue
|
|
||||||
? options.AbsoluteExpiration.Value.LocalDateTime
|
|
||||||
: DateTime.Now.Add(options.AbsoluteExpirationRelativeToNow ?? options.SlidingExpiration ?? TimeSpan.FromDays(1));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DeleteExpiredItems()
|
||||||
|
{
|
||||||
|
if (!scanningExpiration)
|
||||||
|
{
|
||||||
|
scanningExpiration = true;
|
||||||
|
|
||||||
|
foreach (var directory in rootDirectory.EnumerateDirectories())
|
||||||
|
{
|
||||||
|
var deletedFileCount = ScanDirectory(directory);
|
||||||
|
|
||||||
|
if (deletedFileCount > 0)
|
||||||
|
{
|
||||||
|
logger?.LogInformation("Deleted {count} expired items in {name}", deletedFileCount, directory.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scanningExpiration = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ScanDirectory(DirectoryInfo directory)
|
||||||
|
{
|
||||||
|
var deletedFileCount = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
deletedFileCount = directory.EnumerateDirectories().Sum(ScanDirectory);
|
||||||
|
|
||||||
|
foreach (var file in directory.EnumerateFiles()
|
||||||
|
.Where(file => file.CreationTime > file.LastWriteTime &&
|
||||||
|
file.CreationTime <= DateTime.Now))
|
||||||
|
{
|
||||||
|
file.Delete();
|
||||||
|
deletedFileCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!directory.EnumerateFileSystemInfos().Any())
|
||||||
|
{
|
||||||
|
directory.Delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger?.LogError(ex, "Failed cleaning {name}", directory.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedFileCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileInfo GetFile(string key)
|
||||||
|
{
|
||||||
|
FileInfo file = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file = new FileInfo(Path.Combine(rootDirectory.FullName, Path.Combine(key.Split('/'))));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger?.LogError(ex, "Invalid key {key}", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ReadAllBytes(FileInfo file)
|
||||||
|
{
|
||||||
|
using var stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
var buffer = new byte[stream.Length];
|
||||||
|
var offset = 0;
|
||||||
|
|
||||||
|
while (offset < buffer.Length)
|
||||||
|
{
|
||||||
|
offset += stream.Read(buffer, offset, buffer.Length - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<byte[]> ReadAllBytes(FileInfo file, CancellationToken token)
|
||||||
|
{
|
||||||
|
using var stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
var buffer = new byte[stream.Length];
|
||||||
|
var offset = 0;
|
||||||
|
|
||||||
|
while (offset < buffer.Length)
|
||||||
|
{
|
||||||
|
offset += await stream.ReadAsync(buffer, offset, buffer.Length - offset, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetExpiration(FileInfo file, DistributedCacheEntryOptions options)
|
||||||
|
{
|
||||||
|
file.CreationTime = options.AbsoluteExpiration.HasValue
|
||||||
|
? options.AbsoluteExpiration.Value.LocalDateTime
|
||||||
|
: DateTime.Now.Add(options.AbsoluteExpirationRelativeToNow ?? options.SlidingExpiration ?? TimeSpan.FromDays(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,146 +13,145 @@ using Microsoft.UI.Xaml.Media;
|
||||||
using ImageSource = Avalonia.Media.IImage;
|
using ImageSource = Avalonia.Media.IImage;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public static partial class ImageLoader
|
||||||
{
|
{
|
||||||
public static partial class ImageLoader
|
private static ILogger Logger => field ??= LoggerFactory?.CreateLogger(typeof(ImageLoader));
|
||||||
|
|
||||||
|
public static ILoggerFactory LoggerFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The System.Net.Http.HttpClient instance used to download images.
|
||||||
|
/// An application should add a unique User-Agent value to the DefaultRequestHeaders of this
|
||||||
|
/// HttpClient instance (or the Headers of a HttpRequestMessage used in a HttpMessageHandler).
|
||||||
|
/// Failing to set a unique User-Agent value is a violation of OpenStreetMap's tile usage policy
|
||||||
|
/// (see https://operations.osmfoundation.org/policies/tiles/) and results in blocked access
|
||||||
|
/// to their tile servers.
|
||||||
|
/// </summary>
|
||||||
|
public static HttpClient HttpClient
|
||||||
{
|
{
|
||||||
private static ILogger Logger => field ??= LoggerFactory?.CreateLogger(typeof(ImageLoader));
|
get => field ??= new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
public static ILoggerFactory LoggerFactory { get; set; }
|
public static bool IsHttp(this Uri uri)
|
||||||
|
{
|
||||||
|
return uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static async Task<ImageSource> LoadImageAsync(byte[] buffer)
|
||||||
/// The System.Net.Http.HttpClient instance used to download images.
|
{
|
||||||
/// An application should add a unique User-Agent value to the DefaultRequestHeaders of this
|
using var stream = new MemoryStream(buffer);
|
||||||
/// HttpClient instance (or the Headers of a HttpRequestMessage used in a HttpMessageHandler).
|
|
||||||
/// Failing to set a unique User-Agent value is a violation of OpenStreetMap's tile usage policy
|
return await LoadImageAsync(stream);
|
||||||
/// (see https://operations.osmfoundation.org/policies/tiles/) and results in blocked access
|
}
|
||||||
/// to their tile servers.
|
|
||||||
/// </summary>
|
public static async Task<ImageSource> LoadImageAsync(Uri uri, IProgress<double> progress = null)
|
||||||
public static HttpClient HttpClient
|
{
|
||||||
|
ImageSource image = null;
|
||||||
|
|
||||||
|
progress?.Report(0d);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
get => field ??= new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
if (!uri.IsAbsoluteUri)
|
||||||
set;
|
{
|
||||||
|
image = await LoadImageAsync(uri.OriginalString);
|
||||||
|
}
|
||||||
|
else if (uri.IsHttp())
|
||||||
|
{
|
||||||
|
var buffer = await GetHttpContent(uri, progress);
|
||||||
|
|
||||||
|
if (buffer != null)
|
||||||
|
{
|
||||||
|
image = await LoadImageAsync(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (uri.IsFile)
|
||||||
|
{
|
||||||
|
image = await LoadImageAsync(uri.LocalPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
image = LoadResourceImage(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger?.LogError(ex, "Failed loading image from {uri}", uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsHttp(this Uri uri)
|
progress?.Report(1d);
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<HttpResponseMessage> GetHttpResponseAsync(Uri uri, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
return uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps;
|
var response = await HttpClient.GetAsync(uri, completionOption).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger?.LogWarning("{status} ({reason}) from {uri}", (int)response.StatusCode, response.ReasonPhrase, uri);
|
||||||
|
response.Dispose();
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
Logger?.LogWarning("Timeout from {uri}", uri);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger?.LogError(ex, "{uri}", uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<ImageSource> LoadImageAsync(byte[] buffer)
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<byte[]> GetHttpContent(Uri uri, IProgress<double> progress)
|
||||||
|
{
|
||||||
|
var completionOption = progress != null ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead;
|
||||||
|
|
||||||
|
using var response = await GetHttpResponseAsync(uri, completionOption).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (response == null)
|
||||||
{
|
{
|
||||||
using var stream = new MemoryStream(buffer);
|
|
||||||
|
|
||||||
return await LoadImageAsync(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<ImageSource> LoadImageAsync(Uri uri, IProgress<double> progress = null)
|
|
||||||
{
|
|
||||||
ImageSource image = null;
|
|
||||||
|
|
||||||
progress?.Report(0d);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!uri.IsAbsoluteUri)
|
|
||||||
{
|
|
||||||
image = await LoadImageAsync(uri.OriginalString);
|
|
||||||
}
|
|
||||||
else if (uri.IsHttp())
|
|
||||||
{
|
|
||||||
var buffer = await GetHttpContent(uri, progress);
|
|
||||||
|
|
||||||
if (buffer != null)
|
|
||||||
{
|
|
||||||
image = await LoadImageAsync(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (uri.IsFile)
|
|
||||||
{
|
|
||||||
image = await LoadImageAsync(uri.LocalPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
image = LoadResourceImage(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger?.LogError(ex, "Failed loading image from {uri}", uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
progress?.Report(1d);
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<HttpResponseMessage> GetHttpResponseAsync(Uri uri, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await HttpClient.GetAsync(uri, completionOption).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger?.LogWarning("{status} ({reason}) from {uri}", (int)response.StatusCode, response.ReasonPhrase, uri);
|
|
||||||
response.Dispose();
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
Logger?.LogWarning("Timeout from {uri}", uri);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger?.LogError(ex, "{uri}", uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<byte[]> GetHttpContent(Uri uri, IProgress<double> progress)
|
var content = response.Content;
|
||||||
|
var contentLength = content.Headers.ContentLength;
|
||||||
|
|
||||||
|
if (progress == null || !contentLength.HasValue)
|
||||||
{
|
{
|
||||||
var completionOption = progress != null ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead;
|
return await content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
using var response = await GetHttpResponseAsync(uri, completionOption).ConfigureAwait(false);
|
var length = (int)contentLength.Value;
|
||||||
|
var buffer = new byte[length];
|
||||||
|
|
||||||
if (response == null)
|
using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
int offset = 0;
|
||||||
|
int read;
|
||||||
|
|
||||||
|
while (offset < length &&
|
||||||
|
(read = await stream.ReadAsync(buffer, offset, length - offset).ConfigureAwait(false)) > 0)
|
||||||
{
|
{
|
||||||
return null;
|
offset += read;
|
||||||
}
|
|
||||||
|
|
||||||
var content = response.Content;
|
if (offset < length) // 1.0 reported by caller
|
||||||
var contentLength = content.Headers.ContentLength;
|
|
||||||
|
|
||||||
if (progress == null || !contentLength.HasValue)
|
|
||||||
{
|
|
||||||
return await content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var length = (int)contentLength.Value;
|
|
||||||
var buffer = new byte[length];
|
|
||||||
|
|
||||||
using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
int offset = 0;
|
|
||||||
int read;
|
|
||||||
|
|
||||||
while (offset < length &&
|
|
||||||
(read = await stream.ReadAsync(buffer, offset, length - offset).ConfigureAwait(false)) > 0)
|
|
||||||
{
|
{
|
||||||
offset += read;
|
progress.Report((double)offset / length);
|
||||||
|
|
||||||
if (offset < length) // 1.0 reported by caller
|
|
||||||
{
|
|
||||||
progress.Report((double)offset / length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,47 +1,46 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class ImageTileList : List<ImageTile>
|
||||||
{
|
{
|
||||||
public partial class ImageTileList : List<ImageTile>
|
public ImageTileList()
|
||||||
{
|
{
|
||||||
public ImageTileList()
|
}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImageTileList(IEnumerable<ImageTile> source, TileMatrix tileMatrix, int columnCount)
|
public ImageTileList(IEnumerable<ImageTile> source, TileMatrix tileMatrix, int columnCount)
|
||||||
: base(tileMatrix.Width * tileMatrix.Height)
|
: base(tileMatrix.Width * tileMatrix.Height)
|
||||||
{
|
{
|
||||||
FillMatrix(source, tileMatrix.ZoomLevel, tileMatrix.XMin, tileMatrix.YMin, tileMatrix.XMax, tileMatrix.YMax, columnCount);
|
FillMatrix(source, tileMatrix.ZoomLevel, tileMatrix.XMin, tileMatrix.YMin, tileMatrix.XMax, tileMatrix.YMax, columnCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds existing ImageTile from the source collection or newly created ImageTile 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(IEnumerable<ImageTile> source, int zoomLevel, int xMin, int yMin, int xMax, int yMax, int columnCount)
|
public void FillMatrix(IEnumerable<ImageTile> 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++)
|
for (var x = xMin; x <= xMax; x++)
|
||||||
{
|
{
|
||||||
for (var x = xMin; x <= xMax; x++)
|
var tile = source.FirstOrDefault(t => t.ZoomLevel == zoomLevel && t.X == x && t.Y == y);
|
||||||
|
|
||||||
|
if (tile == null)
|
||||||
{
|
{
|
||||||
var tile = source.FirstOrDefault(t => t.ZoomLevel == zoomLevel && t.X == x && t.Y == y);
|
tile = new ImageTile(zoomLevel, x, y, columnCount);
|
||||||
|
|
||||||
if (tile == null)
|
var equivalentTile = source.FirstOrDefault(
|
||||||
|
t => t.Image.Source != null && t.ZoomLevel == tile.ZoomLevel && t.Column == tile.Column && t.Row == tile.Row);
|
||||||
|
|
||||||
|
if (equivalentTile != null)
|
||||||
{
|
{
|
||||||
tile = new ImageTile(zoomLevel, x, y, columnCount);
|
tile.IsPending = false;
|
||||||
|
tile.Image.Source = equivalentTile.Image.Source; // no Opacity animation
|
||||||
var equivalentTile = source.FirstOrDefault(
|
|
||||||
t => t.Image.Source != null && t.ZoomLevel == tile.ZoomLevel && t.Column == tile.Column && t.Row == tile.Row);
|
|
||||||
|
|
||||||
if (equivalentTile != null)
|
|
||||||
{
|
|
||||||
tile.IsPending = false;
|
|
||||||
tile.Image.Source = equivalentTile.Image.Source; // no Opacity animation
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Add(tile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Add(tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,129 +1,128 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A geographic location with latitude and longitude values in degrees.
|
/// A geographic location with latitude and longitude values in degrees.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#if UWP || WINUI
|
#if UWP || WINUI
|
||||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||||
#else
|
#else
|
||||||
[System.ComponentModel.TypeConverter(typeof(LocationConverter))]
|
[System.ComponentModel.TypeConverter(typeof(LocationConverter))]
|
||||||
#endif
|
#endif
|
||||||
public class Location(double latitude, double longitude) : IEquatable<Location>
|
public class Location(double latitude, double longitude) : IEquatable<Location>
|
||||||
|
{
|
||||||
|
public double Latitude { get; } = Math.Min(Math.Max(latitude, -90d), 90d);
|
||||||
|
public double Longitude => longitude;
|
||||||
|
|
||||||
|
public bool LatitudeEquals(double latitude) => Math.Abs(Latitude - latitude) < 1e-9;
|
||||||
|
|
||||||
|
public bool LongitudeEquals(double longitude) => Math.Abs(Longitude - longitude) < 1e-9;
|
||||||
|
|
||||||
|
public bool Equals(double latitude, double longitude) => LatitudeEquals(latitude) && LongitudeEquals(longitude);
|
||||||
|
|
||||||
|
public bool Equals(Location location) => location != null && Equals(location.Latitude, location.Longitude);
|
||||||
|
|
||||||
|
public override bool Equals(object obj) => Equals(obj as Location);
|
||||||
|
|
||||||
|
public override int GetHashCode() => Latitude.GetHashCode() ^ Longitude.GetHashCode();
|
||||||
|
|
||||||
|
public override string ToString() => string.Format(CultureInfo.InvariantCulture, "{0},{1}", Latitude, Longitude);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Location instance from a string containing a comma-separated pair of floating point numbers.
|
||||||
|
/// </summary>
|
||||||
|
public static Location Parse(string location)
|
||||||
{
|
{
|
||||||
public double Latitude { get; } = Math.Min(Math.Max(latitude, -90d), 90d);
|
string[] values = null;
|
||||||
public double Longitude => longitude;
|
|
||||||
|
|
||||||
public bool LatitudeEquals(double latitude) => Math.Abs(Latitude - latitude) < 1e-9;
|
if (!string.IsNullOrEmpty(location))
|
||||||
|
|
||||||
public bool LongitudeEquals(double longitude) => Math.Abs(Longitude - longitude) < 1e-9;
|
|
||||||
|
|
||||||
public bool Equals(double latitude, double longitude) => LatitudeEquals(latitude) && LongitudeEquals(longitude);
|
|
||||||
|
|
||||||
public bool Equals(Location location) => location != null && Equals(location.Latitude, location.Longitude);
|
|
||||||
|
|
||||||
public override bool Equals(object obj) => Equals(obj as Location);
|
|
||||||
|
|
||||||
public override int GetHashCode() => Latitude.GetHashCode() ^ Longitude.GetHashCode();
|
|
||||||
|
|
||||||
public override string ToString() => string.Format(CultureInfo.InvariantCulture, "{0},{1}", Latitude, Longitude);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a Location instance from a string containing a comma-separated pair of floating point numbers.
|
|
||||||
/// </summary>
|
|
||||||
public static Location Parse(string location)
|
|
||||||
{
|
{
|
||||||
string[] values = null;
|
values = location.Split(',');
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(location))
|
|
||||||
{
|
|
||||||
values = location.Split(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values?.Length != 2)
|
|
||||||
{
|
|
||||||
throw new FormatException($"{nameof(Location)} string must contain a comma-separated pair of floating point numbers.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Location(
|
|
||||||
double.Parse(values[0], NumberStyles.Float, CultureInfo.InvariantCulture),
|
|
||||||
double.Parse(values[1], NumberStyles.Float, CultureInfo.InvariantCulture));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (values?.Length != 2)
|
||||||
/// Normalizes a longitude to a value in the interval [-180 .. 180).
|
|
||||||
/// </summary>
|
|
||||||
public static double NormalizeLongitude(double longitude)
|
|
||||||
{
|
{
|
||||||
var x = (longitude + 180d) % 360d;
|
throw new FormatException($"{nameof(Location)} string must contain a comma-separated pair of floating point numbers.");
|
||||||
|
|
||||||
return x < 0d ? x + 180d : x - 180d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arithmetic mean radius (2*a + b) / 3 == (1 - f/3) * a.
|
return new Location(
|
||||||
// See https://en.wikipedia.org/wiki/Earth_radius#Arithmetic_mean_radius.
|
double.Parse(values[0], NumberStyles.Float, CultureInfo.InvariantCulture),
|
||||||
//
|
double.Parse(values[1], NumberStyles.Float, CultureInfo.InvariantCulture));
|
||||||
public const double Wgs84MeanRadius = (1d - MapProjection.Wgs84Flattening / 3d) * MapProjection.Wgs84EquatorialRadius;
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates great circle azimuth in degrees and distance in meters between this and the specified Location.
|
/// Normalizes a longitude to a value in the interval [-180 .. 180).
|
||||||
/// See https://en.wikipedia.org/wiki/Great-circle_navigation#Course.
|
/// </summary>
|
||||||
/// </summary>
|
public static double NormalizeLongitude(double longitude)
|
||||||
public (double, double) GetAzimuthDistance(Location location, double earthRadius = Wgs84MeanRadius)
|
{
|
||||||
{
|
var x = (longitude + 180d) % 360d;
|
||||||
var lat1 = Latitude * Math.PI / 180d;
|
|
||||||
var lon1 = Longitude * Math.PI / 180d;
|
|
||||||
var lat2 = location.Latitude * Math.PI / 180d;
|
|
||||||
var lon2 = location.Longitude * Math.PI / 180d;
|
|
||||||
var cosLat1 = Math.Cos(lat1);
|
|
||||||
var sinLat1 = Math.Sin(lat1);
|
|
||||||
var cosLat2 = Math.Cos(lat2);
|
|
||||||
var sinLat2 = Math.Sin(lat2);
|
|
||||||
var cosLon12 = Math.Cos(lon2 - lon1);
|
|
||||||
var sinLon12 = Math.Sin(lon2 - lon1);
|
|
||||||
var a = cosLat2 * sinLon12;
|
|
||||||
var b = cosLat1 * sinLat2 - sinLat1 * cosLat2 * cosLon12;
|
|
||||||
// α1
|
|
||||||
var azimuth = Math.Atan2(a, b);
|
|
||||||
// σ12
|
|
||||||
var distance = Math.Atan2(Math.Sqrt(a * a + b * b), sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosLon12);
|
|
||||||
|
|
||||||
return (azimuth * 180d / Math.PI, distance * earthRadius);
|
return x < 0d ? x + 180d : x - 180d;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// Arithmetic mean radius (2*a + b) / 3 == (1 - f/3) * a.
|
||||||
/// Calculates great distance in meters between this and the specified Location.
|
// See https://en.wikipedia.org/wiki/Earth_radius#Arithmetic_mean_radius.
|
||||||
/// See https://en.wikipedia.org/wiki/Great-circle_navigation#Course.
|
//
|
||||||
/// </summary>
|
public const double Wgs84MeanRadius = (1d - MapProjection.Wgs84Flattening / 3d) * MapProjection.Wgs84EquatorialRadius;
|
||||||
public double GetDistance(Location location, double earthRadius = Wgs84MeanRadius)
|
|
||||||
{
|
|
||||||
(var _, var distance) = GetAzimuthDistance(location, earthRadius);
|
|
||||||
|
|
||||||
return distance;
|
/// <summary>
|
||||||
}
|
/// Calculates great circle azimuth in degrees and distance in meters between this and the specified Location.
|
||||||
|
/// See https://en.wikipedia.org/wiki/Great-circle_navigation#Course.
|
||||||
|
/// </summary>
|
||||||
|
public (double, double) GetAzimuthDistance(Location location, double earthRadius = Wgs84MeanRadius)
|
||||||
|
{
|
||||||
|
var lat1 = Latitude * Math.PI / 180d;
|
||||||
|
var lon1 = Longitude * Math.PI / 180d;
|
||||||
|
var lat2 = location.Latitude * Math.PI / 180d;
|
||||||
|
var lon2 = location.Longitude * Math.PI / 180d;
|
||||||
|
var cosLat1 = Math.Cos(lat1);
|
||||||
|
var sinLat1 = Math.Sin(lat1);
|
||||||
|
var cosLat2 = Math.Cos(lat2);
|
||||||
|
var sinLat2 = Math.Sin(lat2);
|
||||||
|
var cosLon12 = Math.Cos(lon2 - lon1);
|
||||||
|
var sinLon12 = Math.Sin(lon2 - lon1);
|
||||||
|
var a = cosLat2 * sinLon12;
|
||||||
|
var b = cosLat1 * sinLat2 - sinLat1 * cosLat2 * cosLon12;
|
||||||
|
// α1
|
||||||
|
var azimuth = Math.Atan2(a, b);
|
||||||
|
// σ12
|
||||||
|
var distance = Math.Atan2(Math.Sqrt(a * a + b * b), sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosLon12);
|
||||||
|
|
||||||
/// <summary>
|
return (azimuth * 180d / Math.PI, distance * earthRadius);
|
||||||
/// Calculates the Location on a great circle at the specified azimuth in degrees and distance in meters from this Location.
|
}
|
||||||
/// See https://en.wikipedia.org/wiki/Great-circle_navigation#Finding_way-points.
|
|
||||||
/// </summary>
|
|
||||||
public Location GetLocation(double azimuth, double distance, double earthRadius = Wgs84MeanRadius)
|
|
||||||
{
|
|
||||||
var lat1 = Latitude * Math.PI / 180d;
|
|
||||||
var lon1 = Longitude * Math.PI / 180d;
|
|
||||||
var a = azimuth * Math.PI / 180d;
|
|
||||||
var d = distance / earthRadius;
|
|
||||||
var cosLat1 = Math.Cos(lat1);
|
|
||||||
var sinLat1 = Math.Sin(lat1);
|
|
||||||
var cosA = Math.Cos(a);
|
|
||||||
var sinA = Math.Sin(a);
|
|
||||||
var cosD = Math.Cos(d);
|
|
||||||
var sinD = Math.Sin(d);
|
|
||||||
var lat2 = Math.Asin(sinLat1 * cosD + cosLat1 * sinD * cosA);
|
|
||||||
var lon2 = lon1 + Math.Atan2(sinD * sinA, cosLat1 * cosD - sinLat1 * sinD * cosA);
|
|
||||||
|
|
||||||
return new Location(lat2 * 180d / Math.PI, lon2 * 180d / Math.PI);
|
/// <summary>
|
||||||
}
|
/// Calculates great distance in meters between this and the specified Location.
|
||||||
|
/// See https://en.wikipedia.org/wiki/Great-circle_navigation#Course.
|
||||||
|
/// </summary>
|
||||||
|
public double GetDistance(Location location, double earthRadius = Wgs84MeanRadius)
|
||||||
|
{
|
||||||
|
(var _, var distance) = GetAzimuthDistance(location, earthRadius);
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the Location on a great circle at the specified azimuth in degrees and distance in meters from this Location.
|
||||||
|
/// See https://en.wikipedia.org/wiki/Great-circle_navigation#Finding_way-points.
|
||||||
|
/// </summary>
|
||||||
|
public Location GetLocation(double azimuth, double distance, double earthRadius = Wgs84MeanRadius)
|
||||||
|
{
|
||||||
|
var lat1 = Latitude * Math.PI / 180d;
|
||||||
|
var lon1 = Longitude * Math.PI / 180d;
|
||||||
|
var a = azimuth * Math.PI / 180d;
|
||||||
|
var d = distance / earthRadius;
|
||||||
|
var cosLat1 = Math.Cos(lat1);
|
||||||
|
var sinLat1 = Math.Sin(lat1);
|
||||||
|
var cosA = Math.Cos(a);
|
||||||
|
var sinA = Math.Sin(a);
|
||||||
|
var cosD = Math.Cos(d);
|
||||||
|
var sinD = Math.Sin(d);
|
||||||
|
var lat2 = Math.Asin(sinLat1 * cosD + cosLat1 * sinD * cosA);
|
||||||
|
var lon2 = lon1 + Math.Atan2(sinD * sinA, cosLat1 * cosD - sinLat1 * sinD * cosA);
|
||||||
|
|
||||||
|
return new Location(lat2 * 180d / Math.PI, lon2 * 180d / Math.PI);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,218 +2,217 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A collection of Locations with support for string parsing
|
/// A collection of Locations with support for string parsing
|
||||||
/// and calculation of great circle and rhumb line locations.
|
/// and calculation of great circle and rhumb line locations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#if UWP || WINUI
|
#if UWP || WINUI
|
||||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||||
#else
|
#else
|
||||||
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
||||||
#endif
|
#endif
|
||||||
public partial class LocationCollection : List<Location>
|
public partial class LocationCollection : List<Location>
|
||||||
|
{
|
||||||
|
public LocationCollection()
|
||||||
{
|
{
|
||||||
public LocationCollection()
|
}
|
||||||
|
|
||||||
|
public LocationCollection(IEnumerable<Location> locations)
|
||||||
|
: base(locations)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocationCollection(params Location[] locations)
|
||||||
|
: base(locations)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
if (Count > 0)
|
||||||
{
|
{
|
||||||
|
var deltaLon = longitude - this[Count - 1].Longitude;
|
||||||
|
|
||||||
|
if (deltaLon < -180d)
|
||||||
|
{
|
||||||
|
longitude += 360d;
|
||||||
|
}
|
||||||
|
else if (deltaLon > 180)
|
||||||
|
{
|
||||||
|
longitude -= 360;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocationCollection(IEnumerable<Location> locations)
|
Add(new Location(latitude, longitude));
|
||||||
: base(locations)
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Join(" ", this.Select(l => l.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a LocationCollection instance from a string containing a sequence
|
||||||
|
/// of Location strings that are separated by a spaces or semicolons.
|
||||||
|
/// </summary>
|
||||||
|
public static LocationCollection Parse(string locations)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(locations)
|
||||||
|
? new LocationCollection()
|
||||||
|
: new LocationCollection(locations
|
||||||
|
.Split([' ', ';'], StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(Location.Parse));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a series of Locations on a great circle, i.e. a geodesic that connects
|
||||||
|
/// the two specified Locations, with an optional angular resolution specified in degrees.
|
||||||
|
/// See https://en.wikipedia.org/wiki/Great-circle_navigation.
|
||||||
|
/// </summary>
|
||||||
|
public static LocationCollection GeodesicLocations(Location location1, Location location2, double resolution = 1d)
|
||||||
|
{
|
||||||
|
if (resolution <= 0d)
|
||||||
{
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(resolution), $"The {nameof(resolution)} argument must be greater than zero.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocationCollection(params Location[] locations)
|
var lat1 = location1.Latitude * Math.PI / 180d;
|
||||||
: base(locations)
|
var lon1 = location1.Longitude * Math.PI / 180d;
|
||||||
|
var lat2 = location2.Latitude * Math.PI / 180d;
|
||||||
|
var lon2 = location2.Longitude * Math.PI / 180d;
|
||||||
|
var cosLat1 = Math.Cos(lat1);
|
||||||
|
var sinLat1 = Math.Sin(lat1);
|
||||||
|
var cosLat2 = Math.Cos(lat2);
|
||||||
|
var sinLat2 = Math.Sin(lat2);
|
||||||
|
var cosLon12 = Math.Cos(lon2 - lon1);
|
||||||
|
var sinLon12 = Math.Sin(lon2 - lon1);
|
||||||
|
var a = cosLat2 * sinLon12;
|
||||||
|
var b = cosLat1 * sinLat2 - sinLat1 * cosLat2 * cosLon12;
|
||||||
|
// σ12
|
||||||
|
var s12 = Math.Atan2(Math.Sqrt(a * a + b * b), sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosLon12);
|
||||||
|
|
||||||
|
var n = (int)Math.Ceiling(s12 / resolution * 180d / Math.PI); // s12 in radians
|
||||||
|
|
||||||
|
var locations = new LocationCollection(new Location(location1.Latitude, location1.Longitude));
|
||||||
|
|
||||||
|
if (n > 1)
|
||||||
{
|
{
|
||||||
|
// https://en.wikipedia.org/wiki/Great-circle_navigation#Finding_way-points
|
||||||
|
// α1
|
||||||
|
var az1 = Math.Atan2(a, b);
|
||||||
|
var cosAz1 = Math.Cos(az1);
|
||||||
|
var sinAz1 = Math.Sin(az1);
|
||||||
|
// α0
|
||||||
|
var az0 = Math.Atan2(sinAz1 * cosLat1, Math.Sqrt(cosAz1 * cosAz1 + sinAz1 * sinAz1 * sinLat1 * sinLat1));
|
||||||
|
var cosAz0 = Math.Cos(az0);
|
||||||
|
var sinAz0 = Math.Sin(az0);
|
||||||
|
// σ01
|
||||||
|
var s01 = Math.Atan2(sinLat1, cosLat1 * cosAz1);
|
||||||
|
// λ0
|
||||||
|
var lon0 = lon1 - Math.Atan2(sinAz0 * Math.Sin(s01), Math.Cos(s01));
|
||||||
|
|
||||||
|
for (var i = 1; i < n; i++)
|
||||||
|
{
|
||||||
|
var s = s01 + i * s12 / n;
|
||||||
|
var sinS = Math.Sin(s);
|
||||||
|
var cosS = Math.Cos(s);
|
||||||
|
var lat = Math.Atan2(cosAz0 * sinS, Math.Sqrt(cosS * cosS + sinAz0 * sinAz0 * sinS * sinS));
|
||||||
|
var lon = Math.Atan2(sinAz0 * sinS, cosS) + lon0;
|
||||||
|
|
||||||
|
locations.Add(lat * 180d / Math.PI, lon * 180d / Math.PI);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Add(double latitude, double longitude)
|
locations.Add(location2.Latitude, location2.Longitude);
|
||||||
|
|
||||||
|
return locations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a series of Locations on a rhumb line that connects the two
|
||||||
|
/// specified Locations, with an optional angular resolution specified in degrees.
|
||||||
|
/// See https://en.wikipedia.org/wiki/Rhumb_line.
|
||||||
|
/// </summary>
|
||||||
|
public static LocationCollection RhumblineLocations(Location location1, Location location2, double resolution = 1d)
|
||||||
|
{
|
||||||
|
if (resolution <= 0d)
|
||||||
{
|
{
|
||||||
if (Count > 0)
|
throw new ArgumentOutOfRangeException(
|
||||||
{
|
nameof(resolution), $"The {nameof(resolution)} argument must be greater than zero.");
|
||||||
var deltaLon = longitude - this[Count - 1].Longitude;
|
|
||||||
|
|
||||||
if (deltaLon < -180d)
|
|
||||||
{
|
|
||||||
longitude += 360d;
|
|
||||||
}
|
|
||||||
else if (deltaLon > 180)
|
|
||||||
{
|
|
||||||
longitude -= 360;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Add(new Location(latitude, longitude));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
var lat1 = location1.Latitude;
|
||||||
|
var lon1 = location1.Longitude;
|
||||||
|
var lat2 = location2.Latitude;
|
||||||
|
var lon2 = location2.Longitude;
|
||||||
|
|
||||||
|
var y1 = WebMercatorProjection.LatitudeToY(lat1);
|
||||||
|
var y2 = WebMercatorProjection.LatitudeToY(lat2);
|
||||||
|
|
||||||
|
if (double.IsInfinity(y1))
|
||||||
{
|
{
|
||||||
return string.Join(" ", this.Select(l => l.ToString()));
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(location1), $"The {nameof(location1)} argument must have an absolute latitude value of less than 90.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (double.IsInfinity(y2))
|
||||||
/// Creates a LocationCollection instance from a string containing a sequence
|
|
||||||
/// of Location strings that are separated by a spaces or semicolons.
|
|
||||||
/// </summary>
|
|
||||||
public static LocationCollection Parse(string locations)
|
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(locations)
|
throw new ArgumentOutOfRangeException(
|
||||||
? new LocationCollection()
|
nameof(location2), $"The {nameof(location2)} argument must have an absolute latitude value of less than 90.");
|
||||||
: new LocationCollection(locations
|
|
||||||
.Split([' ', ';'], StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(Location.Parse));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var dlat = lat2 - lat1;
|
||||||
/// Calculates a series of Locations on a great circle, i.e. a geodesic that connects
|
var dlon = lon2 - lon1;
|
||||||
/// the two specified Locations, with an optional angular resolution specified in degrees.
|
var dy = y2 - y1;
|
||||||
/// See https://en.wikipedia.org/wiki/Great-circle_navigation.
|
|
||||||
/// </summary>
|
// beta = atan(dlon,dy)
|
||||||
public static LocationCollection GeodesicLocations(Location location1, Location location2, double resolution = 1d)
|
// sec(beta) = 1 / cos(atan(dlon,dy)) = sqrt(1 + (dlon/dy)^2)
|
||||||
|
//
|
||||||
|
var sec = Math.Sqrt(1d + dlon * dlon / (dy * dy));
|
||||||
|
|
||||||
|
const double secLimit = 1000d; // beta approximately +/-90°
|
||||||
|
|
||||||
|
double s12;
|
||||||
|
|
||||||
|
if (sec > secLimit)
|
||||||
{
|
{
|
||||||
if (resolution <= 0d)
|
var lat = (lat1 + lat2) * Math.PI / 360d; // mean latitude
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(resolution), $"The {nameof(resolution)} argument must be greater than zero.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var lat1 = location1.Latitude * Math.PI / 180d;
|
s12 = Math.Abs(dlon * Math.Cos(lat)); // distance in degrees along parallel of latitude
|
||||||
var lon1 = location1.Longitude * Math.PI / 180d;
|
}
|
||||||
var lat2 = location2.Latitude * Math.PI / 180d;
|
else
|
||||||
var lon2 = location2.Longitude * Math.PI / 180d;
|
{
|
||||||
var cosLat1 = Math.Cos(lat1);
|
s12 = Math.Abs(dlat * sec); // distance in degrees along loxodrome
|
||||||
var sinLat1 = Math.Sin(lat1);
|
|
||||||
var cosLat2 = Math.Cos(lat2);
|
|
||||||
var sinLat2 = Math.Sin(lat2);
|
|
||||||
var cosLon12 = Math.Cos(lon2 - lon1);
|
|
||||||
var sinLon12 = Math.Sin(lon2 - lon1);
|
|
||||||
var a = cosLat2 * sinLon12;
|
|
||||||
var b = cosLat1 * sinLat2 - sinLat1 * cosLat2 * cosLon12;
|
|
||||||
// σ12
|
|
||||||
var s12 = Math.Atan2(Math.Sqrt(a * a + b * b), sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosLon12);
|
|
||||||
|
|
||||||
var n = (int)Math.Ceiling(s12 / resolution * 180d / Math.PI); // s12 in radians
|
|
||||||
|
|
||||||
var locations = new LocationCollection(new Location(location1.Latitude, location1.Longitude));
|
|
||||||
|
|
||||||
if (n > 1)
|
|
||||||
{
|
|
||||||
// https://en.wikipedia.org/wiki/Great-circle_navigation#Finding_way-points
|
|
||||||
// α1
|
|
||||||
var az1 = Math.Atan2(a, b);
|
|
||||||
var cosAz1 = Math.Cos(az1);
|
|
||||||
var sinAz1 = Math.Sin(az1);
|
|
||||||
// α0
|
|
||||||
var az0 = Math.Atan2(sinAz1 * cosLat1, Math.Sqrt(cosAz1 * cosAz1 + sinAz1 * sinAz1 * sinLat1 * sinLat1));
|
|
||||||
var cosAz0 = Math.Cos(az0);
|
|
||||||
var sinAz0 = Math.Sin(az0);
|
|
||||||
// σ01
|
|
||||||
var s01 = Math.Atan2(sinLat1, cosLat1 * cosAz1);
|
|
||||||
// λ0
|
|
||||||
var lon0 = lon1 - Math.Atan2(sinAz0 * Math.Sin(s01), Math.Cos(s01));
|
|
||||||
|
|
||||||
for (var i = 1; i < n; i++)
|
|
||||||
{
|
|
||||||
var s = s01 + i * s12 / n;
|
|
||||||
var sinS = Math.Sin(s);
|
|
||||||
var cosS = Math.Cos(s);
|
|
||||||
var lat = Math.Atan2(cosAz0 * sinS, Math.Sqrt(cosS * cosS + sinAz0 * sinAz0 * sinS * sinS));
|
|
||||||
var lon = Math.Atan2(sinAz0 * sinS, cosS) + lon0;
|
|
||||||
|
|
||||||
locations.Add(lat * 180d / Math.PI, lon * 180d / Math.PI);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
locations.Add(location2.Latitude, location2.Longitude);
|
|
||||||
|
|
||||||
return locations;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var n = (int)Math.Ceiling(s12 / resolution);
|
||||||
/// Calculates a series of Locations on a rhumb line that connects the two
|
|
||||||
/// specified Locations, with an optional angular resolution specified in degrees.
|
var locations = new LocationCollection(new Location(lat1, lon1));
|
||||||
/// See https://en.wikipedia.org/wiki/Rhumb_line.
|
|
||||||
/// </summary>
|
if (sec > secLimit)
|
||||||
public static LocationCollection RhumblineLocations(Location location1, Location location2, double resolution = 1d)
|
|
||||||
{
|
{
|
||||||
if (resolution <= 0d)
|
for (var i = 1; i < n; i++)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(
|
var lon = lon1 + i * dlon / n;
|
||||||
nameof(resolution), $"The {nameof(resolution)} argument must be greater than zero.");
|
var lat = WebMercatorProjection.YToLatitude(y1 + i * dy / n);
|
||||||
|
locations.Add(lat, lon);
|
||||||
}
|
}
|
||||||
|
|
||||||
var lat1 = location1.Latitude;
|
|
||||||
var lon1 = location1.Longitude;
|
|
||||||
var lat2 = location2.Latitude;
|
|
||||||
var lon2 = location2.Longitude;
|
|
||||||
|
|
||||||
var y1 = WebMercatorProjection.LatitudeToY(lat1);
|
|
||||||
var y2 = WebMercatorProjection.LatitudeToY(lat2);
|
|
||||||
|
|
||||||
if (double.IsInfinity(y1))
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(location1), $"The {nameof(location1)} argument must have an absolute latitude value of less than 90.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (double.IsInfinity(y2))
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(location2), $"The {nameof(location2)} argument must have an absolute latitude value of less than 90.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var dlat = lat2 - lat1;
|
|
||||||
var dlon = lon2 - lon1;
|
|
||||||
var dy = y2 - y1;
|
|
||||||
|
|
||||||
// beta = atan(dlon,dy)
|
|
||||||
// sec(beta) = 1 / cos(atan(dlon,dy)) = sqrt(1 + (dlon/dy)^2)
|
|
||||||
//
|
|
||||||
var sec = Math.Sqrt(1d + dlon * dlon / (dy * dy));
|
|
||||||
|
|
||||||
const double secLimit = 1000d; // beta approximately +/-90°
|
|
||||||
|
|
||||||
double s12;
|
|
||||||
|
|
||||||
if (sec > secLimit)
|
|
||||||
{
|
|
||||||
var lat = (lat1 + lat2) * Math.PI / 360d; // mean latitude
|
|
||||||
|
|
||||||
s12 = Math.Abs(dlon * Math.Cos(lat)); // distance in degrees along parallel of latitude
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
s12 = Math.Abs(dlat * sec); // distance in degrees along loxodrome
|
|
||||||
}
|
|
||||||
|
|
||||||
var n = (int)Math.Ceiling(s12 / resolution);
|
|
||||||
|
|
||||||
var locations = new LocationCollection(new Location(lat1, lon1));
|
|
||||||
|
|
||||||
if (sec > secLimit)
|
|
||||||
{
|
|
||||||
for (var i = 1; i < n; i++)
|
|
||||||
{
|
|
||||||
var lon = lon1 + i * dlon / n;
|
|
||||||
var lat = WebMercatorProjection.YToLatitude(y1 + i * dy / n);
|
|
||||||
locations.Add(lat, lon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (var i = 1; i < n; i++)
|
|
||||||
{
|
|
||||||
var lat = lat1 + i * dlat / n;
|
|
||||||
var lon = lon1 + dlon * (WebMercatorProjection.LatitudeToY(lat) - y1) / dy;
|
|
||||||
locations.Add(lat, lon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
locations.Add(lat2, lon2);
|
|
||||||
|
|
||||||
return locations;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = 1; i < n; i++)
|
||||||
|
{
|
||||||
|
var lat = lat1 + i * dlat / n;
|
||||||
|
var lon = lon1 + dlon * (WebMercatorProjection.LatitudeToY(lat) - y1) / dy;
|
||||||
|
locations.Add(lat, lon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locations.Add(lat2, lon2);
|
||||||
|
|
||||||
|
return locations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,54 +9,53 @@ using Microsoft.UI.Xaml;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MapBase with default input event handling.
|
||||||
|
/// </summary>
|
||||||
|
public partial class Map : MapBase
|
||||||
{
|
{
|
||||||
|
public static readonly DependencyProperty MouseWheelZoomDeltaProperty =
|
||||||
|
DependencyPropertyHelper.Register<Map, double>(nameof(MouseWheelZoomDelta), 0.25);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MouseWheelZoomAnimatedProperty =
|
||||||
|
DependencyPropertyHelper.Register<Map, bool>(nameof(MouseWheelZoomAnimated), true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MapBase with default input event handling.
|
/// Gets or sets the amount by which the ZoomLevel property changes by a MouseWheel event.
|
||||||
|
/// The default value is 0.25.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class Map : MapBase
|
public double MouseWheelZoomDelta
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty MouseWheelZoomDeltaProperty =
|
get => (double)GetValue(MouseWheelZoomDeltaProperty);
|
||||||
DependencyPropertyHelper.Register<Map, double>(nameof(MouseWheelZoomDelta), 0.25);
|
set => SetValue(MouseWheelZoomDeltaProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty MouseWheelZoomAnimatedProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<Map, bool>(nameof(MouseWheelZoomAnimated), true);
|
/// Gets or sets a value that specifies whether zooming by a MouseWheel event is animated.
|
||||||
|
/// The default value is true.
|
||||||
|
/// </summary>
|
||||||
|
public bool MouseWheelZoomAnimated
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(MouseWheelZoomAnimatedProperty);
|
||||||
|
set => SetValue(MouseWheelZoomAnimatedProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void OnMouseWheel(Point position, double delta)
|
||||||
/// Gets or sets the amount by which the ZoomLevel property changes by a MouseWheel event.
|
{
|
||||||
/// The default value is 0.25.
|
var zoomLevel = TargetZoomLevel + MouseWheelZoomDelta * delta;
|
||||||
/// </summary>
|
var animated = false;
|
||||||
public double MouseWheelZoomDelta
|
|
||||||
|
if (delta <= -1d || delta >= 1d)
|
||||||
{
|
{
|
||||||
get => (double)GetValue(MouseWheelZoomDeltaProperty);
|
// Zoom to integer multiple of MouseWheelZoomDelta when the event was raised by a
|
||||||
set => SetValue(MouseWheelZoomDeltaProperty, value);
|
// mouse wheel or by a large movement on a touch pad or other high resolution device.
|
||||||
|
//
|
||||||
|
zoomLevel = MouseWheelZoomDelta * Math.Round(zoomLevel / MouseWheelZoomDelta);
|
||||||
|
animated = MouseWheelZoomAnimated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
ZoomMap(position, zoomLevel, animated);
|
||||||
/// Gets or sets a value that specifies whether zooming by a MouseWheel event is animated.
|
|
||||||
/// The default value is true.
|
|
||||||
/// </summary>
|
|
||||||
public bool MouseWheelZoomAnimated
|
|
||||||
{
|
|
||||||
get => (bool)GetValue(MouseWheelZoomAnimatedProperty);
|
|
||||||
set => SetValue(MouseWheelZoomAnimatedProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMouseWheel(Point position, double delta)
|
|
||||||
{
|
|
||||||
var zoomLevel = TargetZoomLevel + MouseWheelZoomDelta * delta;
|
|
||||||
var animated = false;
|
|
||||||
|
|
||||||
if (delta <= -1d || delta >= 1d)
|
|
||||||
{
|
|
||||||
// Zoom to integer multiple of MouseWheelZoomDelta when the event was raised by a
|
|
||||||
// mouse wheel or by a large movement on a touch pad or other high resolution device.
|
|
||||||
//
|
|
||||||
zoomLevel = MouseWheelZoomDelta * Math.Round(zoomLevel / MouseWheelZoomDelta);
|
|
||||||
animated = MouseWheelZoomAnimated;
|
|
||||||
}
|
|
||||||
|
|
||||||
ZoomMap(position, zoomLevel, animated);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,240 +17,239 @@ using Avalonia.Controls;
|
||||||
using Brush = Avalonia.Media.IBrush;
|
using Brush = Avalonia.Media.IBrush;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public interface IMapLayer : IMapElement
|
||||||
{
|
{
|
||||||
public interface IMapLayer : IMapElement
|
Brush MapBackground { get; }
|
||||||
|
Brush MapForeground { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class MapBase
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty MapLayerProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, object>(nameof(MapLayer), null,
|
||||||
|
(map, oldValue, newValue) => map.MapLayerPropertyChanged(oldValue, newValue));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MapLayersSourceProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, IEnumerable>(nameof(MapLayersSource), null,
|
||||||
|
(map, oldValue, newValue) => map.MapLayersSourcePropertyChanged(oldValue, newValue));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the base map layer, which is added as first element to the Children collection.
|
||||||
|
/// If the passed object is not a FrameworkElement, MapBase tries to locate a DataTemplate
|
||||||
|
/// resource for the object's type and generate a FrameworkElement from that DataTemplate.
|
||||||
|
/// If the FrameworkElement implements IMapLayer (like e.g. TilePyramidLayer or MapImageLayer),
|
||||||
|
/// its (non-null) MapBackground and MapForeground property values are used for the MapBase
|
||||||
|
/// Background and Foreground.
|
||||||
|
/// </summary>
|
||||||
|
public object MapLayer
|
||||||
{
|
{
|
||||||
Brush MapBackground { get; }
|
get => GetValue(MapLayerProperty);
|
||||||
Brush MapForeground { get; }
|
set => SetValue(MapLayerProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class MapBase
|
/// <summary>
|
||||||
|
/// Holds a collection of map layers, either FrameworkElements or plain objects with
|
||||||
|
/// an associated DataTemplate resource from which a FrameworkElement can be created.
|
||||||
|
/// FrameworkElemens are added to the Children collection, starting at index 0.
|
||||||
|
/// The first element of this collection is assigned to the MapLayer property.
|
||||||
|
/// Subsequent changes of the MapLayer or Children properties are not reflected
|
||||||
|
/// by the MapLayersSource collection.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable MapLayersSource
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty MapLayerProperty =
|
get => (IEnumerable)GetValue(MapLayersSourceProperty);
|
||||||
DependencyPropertyHelper.Register<MapBase, object>(nameof(MapLayer), null,
|
set => SetValue(MapLayersSourceProperty, value);
|
||||||
(map, oldValue, newValue) => map.MapLayerPropertyChanged(oldValue, newValue));
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty MapLayersSourceProperty =
|
private void MapLayerPropertyChanged(object oldLayer, object newLayer)
|
||||||
DependencyPropertyHelper.Register<MapBase, IEnumerable>(nameof(MapLayersSource), null,
|
{
|
||||||
(map, oldValue, newValue) => map.MapLayersSourcePropertyChanged(oldValue, newValue));
|
bool IsMapLayer(object layer) => Children.Count > 0 &&
|
||||||
|
(Children[0] == layer as FrameworkElement ||
|
||||||
|
((FrameworkElement)Children[0]).DataContext == layer);
|
||||||
|
|
||||||
/// <summary>
|
if (oldLayer != null && IsMapLayer(oldLayer))
|
||||||
/// Gets or sets the base map layer, which is added as first element to the Children collection.
|
|
||||||
/// If the passed object is not a FrameworkElement, MapBase tries to locate a DataTemplate
|
|
||||||
/// resource for the object's type and generate a FrameworkElement from that DataTemplate.
|
|
||||||
/// If the FrameworkElement implements IMapLayer (like e.g. TilePyramidLayer or MapImageLayer),
|
|
||||||
/// its (non-null) MapBackground and MapForeground property values are used for the MapBase
|
|
||||||
/// Background and Foreground.
|
|
||||||
/// </summary>
|
|
||||||
public object MapLayer
|
|
||||||
{
|
{
|
||||||
get => GetValue(MapLayerProperty);
|
RemoveChildElement(0);
|
||||||
set => SetValue(MapLayerProperty, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (newLayer != null && !IsMapLayer(newLayer))
|
||||||
/// Holds a collection of map layers, either FrameworkElements or plain objects with
|
|
||||||
/// an associated DataTemplate resource from which a FrameworkElement can be created.
|
|
||||||
/// FrameworkElemens are added to the Children collection, starting at index 0.
|
|
||||||
/// The first element of this collection is assigned to the MapLayer property.
|
|
||||||
/// Subsequent changes of the MapLayer or Children properties are not reflected
|
|
||||||
/// by the MapLayersSource collection.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable MapLayersSource
|
|
||||||
{
|
{
|
||||||
get => (IEnumerable)GetValue(MapLayersSourceProperty);
|
InsertChildElement(0, GetMapLayer(newLayer));
|
||||||
set => SetValue(MapLayersSourceProperty, value);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MapLayersSourcePropertyChanged(IEnumerable oldLayers, IEnumerable newLayers)
|
||||||
|
{
|
||||||
|
if (oldLayers != null)
|
||||||
|
{
|
||||||
|
if (oldLayers is INotifyCollectionChanged incc)
|
||||||
|
{
|
||||||
|
incc.CollectionChanged -= MapLayersSourceCollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveMapLayers(oldLayers, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MapLayerPropertyChanged(object oldLayer, object newLayer)
|
if (newLayers != null)
|
||||||
{
|
{
|
||||||
bool IsMapLayer(object layer) => Children.Count > 0 &&
|
if (newLayers is INotifyCollectionChanged incc)
|
||||||
(Children[0] == layer as FrameworkElement ||
|
|
||||||
((FrameworkElement)Children[0]).DataContext == layer);
|
|
||||||
|
|
||||||
if (oldLayer != null && IsMapLayer(oldLayer))
|
|
||||||
{
|
{
|
||||||
RemoveChildElement(0);
|
incc.CollectionChanged += MapLayersSourceCollectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newLayer != null && !IsMapLayer(newLayer))
|
AddMapLayers(newLayers, 0);
|
||||||
{
|
|
||||||
InsertChildElement(0, GetMapLayer(newLayer));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void MapLayersSourcePropertyChanged(IEnumerable oldLayers, IEnumerable newLayers)
|
private void MapLayersSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
if (oldLayers != null)
|
case NotifyCollectionChangedAction.Add:
|
||||||
{
|
AddMapLayers(e.NewItems, e.NewStartingIndex);
|
||||||
if (oldLayers is INotifyCollectionChanged incc)
|
break;
|
||||||
{
|
|
||||||
incc.CollectionChanged -= MapLayersSourceCollectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveMapLayers(oldLayers, 0);
|
case NotifyCollectionChangedAction.Remove:
|
||||||
}
|
RemoveMapLayers(e.OldItems, e.OldStartingIndex);
|
||||||
|
break;
|
||||||
|
|
||||||
if (newLayers != null)
|
case NotifyCollectionChangedAction.Replace:
|
||||||
{
|
RemoveMapLayers(e.OldItems, e.OldStartingIndex);
|
||||||
if (newLayers is INotifyCollectionChanged incc)
|
AddMapLayers(e.NewItems, e.NewStartingIndex);
|
||||||
{
|
break;
|
||||||
incc.CollectionChanged += MapLayersSourceCollectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
AddMapLayers(newLayers, 0);
|
case NotifyCollectionChangedAction.Reset:
|
||||||
}
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void MapLayersSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
private void AddMapLayers(IEnumerable layers, int index)
|
||||||
|
{
|
||||||
|
var mapLayers = layers.Cast<object>().Select(GetMapLayer).ToList();
|
||||||
|
|
||||||
|
if (mapLayers.Count > 0)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
|
||||||
{
|
|
||||||
case NotifyCollectionChangedAction.Add:
|
|
||||||
AddMapLayers(e.NewItems, e.NewStartingIndex);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Remove:
|
|
||||||
RemoveMapLayers(e.OldItems, e.OldStartingIndex);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Replace:
|
|
||||||
RemoveMapLayers(e.OldItems, e.OldStartingIndex);
|
|
||||||
AddMapLayers(e.NewItems, e.NewStartingIndex);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Reset:
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddMapLayers(IEnumerable layers, int index)
|
|
||||||
{
|
|
||||||
var mapLayers = layers.Cast<object>().Select(GetMapLayer).ToList();
|
|
||||||
|
|
||||||
if (mapLayers.Count > 0)
|
|
||||||
{
|
|
||||||
#if WPF // Execute at DispatcherPriority.DataBind to ensure that all bindings are evaluated.
|
#if WPF // Execute at DispatcherPriority.DataBind to ensure that all bindings are evaluated.
|
||||||
//
|
//
|
||||||
Dispatcher.Invoke(() => AddMapLayers(mapLayers, index), DispatcherPriority.DataBind);
|
Dispatcher.Invoke(() => AddMapLayers(mapLayers, index), DispatcherPriority.DataBind);
|
||||||
#else
|
#else
|
||||||
AddMapLayers(mapLayers, index);
|
AddMapLayers(mapLayers, index);
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddMapLayers(List<FrameworkElement> mapLayers, int index)
|
||||||
|
{
|
||||||
|
foreach (var mapLayer in mapLayers)
|
||||||
|
{
|
||||||
|
InsertChildElement(index, mapLayer);
|
||||||
|
|
||||||
|
if (index++ == 0)
|
||||||
|
{
|
||||||
|
MapLayer = mapLayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveMapLayers(IEnumerable layers, int index)
|
||||||
|
{
|
||||||
|
foreach (var _ in layers)
|
||||||
|
{
|
||||||
|
RemoveChildElement(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == 0)
|
||||||
|
{
|
||||||
|
MapLayer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InsertChildElement(int index, FrameworkElement element)
|
||||||
|
{
|
||||||
|
if (index == 0 && element is IMapLayer mapLayer)
|
||||||
|
{
|
||||||
|
if (mapLayer.MapBackground != null)
|
||||||
|
{
|
||||||
|
Background = mapLayer.MapBackground;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapLayer.MapForeground != null)
|
||||||
|
{
|
||||||
|
Foreground = mapLayer.MapForeground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddMapLayers(List<FrameworkElement> mapLayers, int index)
|
Children.Insert(index, element);
|
||||||
{
|
}
|
||||||
foreach (var mapLayer in mapLayers)
|
|
||||||
{
|
|
||||||
InsertChildElement(index, mapLayer);
|
|
||||||
|
|
||||||
if (index++ == 0)
|
private void RemoveChildElement(int index)
|
||||||
{
|
{
|
||||||
MapLayer = mapLayer;
|
if (index == 0 && Children[0] is IMapLayer mapLayer)
|
||||||
}
|
{
|
||||||
|
if (mapLayer.MapBackground != null)
|
||||||
|
{
|
||||||
|
ClearValue(BackgroundProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapLayer.MapForeground != null)
|
||||||
|
{
|
||||||
|
ClearValue(ForegroundProperty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveMapLayers(IEnumerable layers, int index)
|
Children.RemoveAt(index);
|
||||||
{
|
}
|
||||||
foreach (var _ in layers)
|
|
||||||
{
|
|
||||||
RemoveChildElement(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index == 0)
|
private FrameworkElement GetMapLayer(object layer)
|
||||||
{
|
{
|
||||||
MapLayer = null;
|
FrameworkElement mapLayer = null;
|
||||||
}
|
|
||||||
|
if (layer != null)
|
||||||
|
{
|
||||||
|
mapLayer = layer as FrameworkElement ?? TryLoadDataTemplate(layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertChildElement(int index, FrameworkElement element)
|
return mapLayer ?? new MapTileLayer();
|
||||||
{
|
}
|
||||||
if (index == 0 && element is IMapLayer mapLayer)
|
|
||||||
{
|
|
||||||
if (mapLayer.MapBackground != null)
|
|
||||||
{
|
|
||||||
Background = mapLayer.MapBackground;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mapLayer.MapForeground != null)
|
private FrameworkElement TryLoadDataTemplate(object layer)
|
||||||
{
|
{
|
||||||
Foreground = mapLayer.MapForeground;
|
FrameworkElement element = null;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Children.Insert(index, element);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveChildElement(int index)
|
|
||||||
{
|
|
||||||
if (index == 0 && Children[0] is IMapLayer mapLayer)
|
|
||||||
{
|
|
||||||
if (mapLayer.MapBackground != null)
|
|
||||||
{
|
|
||||||
ClearValue(BackgroundProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mapLayer.MapForeground != null)
|
|
||||||
{
|
|
||||||
ClearValue(ForegroundProperty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Children.RemoveAt(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
private FrameworkElement GetMapLayer(object layer)
|
|
||||||
{
|
|
||||||
FrameworkElement mapLayer = null;
|
|
||||||
|
|
||||||
if (layer != null)
|
|
||||||
{
|
|
||||||
mapLayer = layer as FrameworkElement ?? TryLoadDataTemplate(layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapLayer ?? new MapTileLayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
private FrameworkElement TryLoadDataTemplate(object layer)
|
|
||||||
{
|
|
||||||
FrameworkElement element = null;
|
|
||||||
#if AVALONIA
|
#if AVALONIA
|
||||||
if (this.TryFindResource(layer.GetType().FullName, out object value) &&
|
if (this.TryFindResource(layer.GetType().FullName, out object value) &&
|
||||||
value is Avalonia.Markup.Xaml.Templates.DataTemplate template)
|
value is Avalonia.Markup.Xaml.Templates.DataTemplate template)
|
||||||
{
|
{
|
||||||
element = template.Build(layer);
|
element = template.Build(layer);
|
||||||
}
|
|
||||||
#elif WPF
|
|
||||||
if (TryFindResource(new DataTemplateKey(layer.GetType())) is DataTemplate template)
|
|
||||||
{
|
|
||||||
element = (FrameworkElement)template.LoadContent();
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
if (TryFindResource(this, layer.GetType().FullName) is DataTemplate template)
|
|
||||||
{
|
|
||||||
element = (FrameworkElement)template.LoadContent();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
element?.DataContext = layer;
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
}
|
||||||
|
#elif WPF
|
||||||
|
if (TryFindResource(new DataTemplateKey(layer.GetType())) is DataTemplate template)
|
||||||
|
{
|
||||||
|
element = (FrameworkElement)template.LoadContent();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (TryFindResource(this, layer.GetType().FullName) is DataTemplate template)
|
||||||
|
{
|
||||||
|
element = (FrameworkElement)template.LoadContent();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
element?.DataContext = layer;
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
#if UWP || WINUI
|
#if UWP || WINUI
|
||||||
private static object TryFindResource(FrameworkElement element, object key)
|
private static object TryFindResource(FrameworkElement element, object key)
|
||||||
{
|
{
|
||||||
return element.Resources.ContainsKey(key)
|
return element.Resources.ContainsKey(key)
|
||||||
? element.Resources[key]
|
? element.Resources[key]
|
||||||
: element.Parent is FrameworkElement parent
|
: element.Parent is FrameworkElement parent
|
||||||
? TryFindResource(parent, key)
|
? TryFindResource(parent, key)
|
||||||
: null;
|
: null;
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,431 +13,430 @@ using Avalonia;
|
||||||
using Brush = Avalonia.Media.IBrush;
|
using Brush = Avalonia.Media.IBrush;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The map control. Displays map content provided by one or more layers like MapTileLayer,
|
||||||
|
/// WmtsTileLayer or WmsImageLayer. The visible map area is defined by the Center and
|
||||||
|
/// ZoomLevel properties. The map can be rotated by an angle provided by the Heading property.
|
||||||
|
/// MapBase can contain map overlay child elements like other MapPanels or MapItemsControls.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapBase : MapPanel
|
||||||
{
|
{
|
||||||
/// <summary>
|
public static double ZoomLevelToScale(double zoomLevel)
|
||||||
/// The map control. Displays map content provided by one or more layers like MapTileLayer,
|
|
||||||
/// WmtsTileLayer or WmsImageLayer. The visible map area is defined by the Center and
|
|
||||||
/// ZoomLevel properties. The map can be rotated by an angle provided by the Heading property.
|
|
||||||
/// MapBase can contain map overlay child elements like other MapPanels or MapItemsControls.
|
|
||||||
/// </summary>
|
|
||||||
public partial class MapBase : MapPanel
|
|
||||||
{
|
{
|
||||||
public static double ZoomLevelToScale(double zoomLevel)
|
return 256d * Math.Pow(2d, zoomLevel) / (360d * MapProjection.Wgs84MeterPerDegree);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double ScaleToZoomLevel(double scale)
|
||||||
|
{
|
||||||
|
return Math.Log(scale * 360d * MapProjection.Wgs84MeterPerDegree / 256d, 2d);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeSpan ImageFadeDuration { get; set; } = TimeSpan.FromSeconds(0.2);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty AnimationDurationProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, TimeSpan>(nameof(AnimationDuration), TimeSpan.FromSeconds(0.3));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MapProjectionProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, MapProjection>(nameof(MapProjection), new WebMercatorProjection(),
|
||||||
|
(map, oldValue, newValue) => map.MapProjectionPropertyChanged(newValue));
|
||||||
|
|
||||||
|
private Location transformCenter;
|
||||||
|
private Point viewCenter;
|
||||||
|
private double centerLongitude;
|
||||||
|
private double maxLatitude = 85.05112878; // default WebMercatorProjection
|
||||||
|
private bool internalPropertyChange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when the current map viewport has changed.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<ViewportChangedEventArgs> ViewportChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the map foreground Brush.
|
||||||
|
/// </summary>
|
||||||
|
public Brush Foreground
|
||||||
|
{
|
||||||
|
get => (Brush)GetValue(ForegroundProperty);
|
||||||
|
set => SetValue(ForegroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the duration of the Center, ZoomLevel and Heading animations.
|
||||||
|
/// The default value is 0.3 seconds.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan AnimationDuration
|
||||||
|
{
|
||||||
|
get => (TimeSpan)GetValue(AnimationDurationProperty);
|
||||||
|
set => SetValue(AnimationDurationProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the MapProjection used by the map control.
|
||||||
|
/// </summary>
|
||||||
|
public MapProjection MapProjection
|
||||||
|
{
|
||||||
|
get => (MapProjection)GetValue(MapProjectionProperty);
|
||||||
|
set => SetValue(MapProjectionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the location of the center point of the map.
|
||||||
|
/// </summary>
|
||||||
|
public Location Center
|
||||||
|
{
|
||||||
|
get => (Location)GetValue(CenterProperty);
|
||||||
|
set => SetValue(CenterProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the target value of a Center animation.
|
||||||
|
/// </summary>
|
||||||
|
public Location TargetCenter
|
||||||
|
{
|
||||||
|
get => (Location)GetValue(TargetCenterProperty);
|
||||||
|
set => SetValue(TargetCenterProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum value of the ZoomLevel and TargetZoomLevel properties.
|
||||||
|
/// Must not be less than zero or greater than MaxZoomLevel. The default value is 1.
|
||||||
|
/// </summary>
|
||||||
|
public double MinZoomLevel
|
||||||
|
{
|
||||||
|
get => (double)GetValue(MinZoomLevelProperty);
|
||||||
|
set => SetValue(MinZoomLevelProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum value of the ZoomLevel and TargetZoomLevel properties.
|
||||||
|
/// Must not be less than MinZoomLevel. The default value is 20.
|
||||||
|
/// </summary>
|
||||||
|
public double MaxZoomLevel
|
||||||
|
{
|
||||||
|
get => (double)GetValue(MaxZoomLevelProperty);
|
||||||
|
set => SetValue(MaxZoomLevelProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the map zoom level.
|
||||||
|
/// </summary>
|
||||||
|
public double ZoomLevel
|
||||||
|
{
|
||||||
|
get => (double)GetValue(ZoomLevelProperty);
|
||||||
|
set => SetValue(ZoomLevelProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the target value of a ZoomLevel animation.
|
||||||
|
/// </summary>
|
||||||
|
public double TargetZoomLevel
|
||||||
|
{
|
||||||
|
get => (double)GetValue(TargetZoomLevelProperty);
|
||||||
|
set => SetValue(TargetZoomLevelProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the map heading, a counter-clockwise rotation angle in degrees.
|
||||||
|
/// </summary>
|
||||||
|
public double Heading
|
||||||
|
{
|
||||||
|
get => (double)GetValue(HeadingProperty);
|
||||||
|
set => SetValue(HeadingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the target value of a Heading animation.
|
||||||
|
/// </summary>
|
||||||
|
public double TargetHeading
|
||||||
|
{
|
||||||
|
get => (double)GetValue(TargetHeadingProperty);
|
||||||
|
set => SetValue(TargetHeadingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ViewTransform instance that is used to transform between
|
||||||
|
/// projected map coordinates and view coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public ViewTransform ViewTransform { get; } = new ViewTransform();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a transform Matrix from meters to view coordinates for scaling and rotating
|
||||||
|
/// at the specified geographic coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Matrix GetMapToViewTransform(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
var transform = MapProjection.RelativeTransform(latitude, longitude);
|
||||||
|
transform.Scale(ViewTransform.Scale, ViewTransform.Scale);
|
||||||
|
transform.Rotate(ViewTransform.Rotation);
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a transform Matrix from meters to view coordinates for scaling and rotating
|
||||||
|
/// at the specified Location.
|
||||||
|
/// </summary>
|
||||||
|
public Matrix GetMapToViewTransform(Location location) => GetMapToViewTransform(location.Latitude, location.Longitude);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms geographic coordinates to a Point in view coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Point LocationToView(double latitude, double longitude) => ViewTransform.MapToView(MapProjection.LocationToMap(latitude, longitude));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms a Location in geographic coordinates to a Point in view coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Point LocationToView(Location location) => LocationToView(location.Latitude, location.Longitude);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms a Point in view coordinates to a Location in geographic coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Location ViewToLocation(Point point) => MapProjection.MapToLocation(ViewTransform.ViewToMap(point));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a temporary center point in view coordinates for scaling and rotation transformations.
|
||||||
|
/// This center point is automatically reset when the Center property is set by application code
|
||||||
|
/// or by the methods TranslateMap, TransformMap, ZoomMap and ZoomToBounds.
|
||||||
|
/// </summary>
|
||||||
|
public void SetTransformCenter(Point center)
|
||||||
|
{
|
||||||
|
viewCenter = center;
|
||||||
|
transformCenter = ViewToLocation(center);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the temporary transform center point set by SetTransformCenter.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetTransformCenter()
|
||||||
|
{
|
||||||
|
viewCenter = new Point(ActualWidth / 2d, ActualHeight / 2d);
|
||||||
|
transformCenter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes the Center property according to the specified translation in view coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public void TranslateMap(Point translation)
|
||||||
|
{
|
||||||
|
if (translation.X != 0d || translation.Y != 0d)
|
||||||
{
|
{
|
||||||
return 256d * Math.Pow(2d, zoomLevel) / (360d * MapProjection.Wgs84MeterPerDegree);
|
Center = ViewToLocation(new Point(viewCenter.X - translation.X, viewCenter.Y - translation.Y));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static double ScaleToZoomLevel(double scale)
|
/// <summary>
|
||||||
|
/// Changes the Center, Heading and ZoomLevel properties according to the specified
|
||||||
|
/// view coordinate translation, rotation and scale delta values. Rotation and scaling
|
||||||
|
/// is performed relative to the specified center point in view coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public void TransformMap(Point center, Point translation, double rotation, double scale)
|
||||||
|
{
|
||||||
|
if (rotation == 0d && scale == 1d)
|
||||||
{
|
{
|
||||||
return Math.Log(scale * 360d * MapProjection.Wgs84MeterPerDegree / 256d, 2d);
|
TranslateMap(translation);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
public static TimeSpan ImageFadeDuration { get; set; } = TimeSpan.FromSeconds(0.2);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty AnimationDurationProperty =
|
|
||||||
DependencyPropertyHelper.Register<MapBase, TimeSpan>(nameof(AnimationDuration), TimeSpan.FromSeconds(0.3));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MapProjectionProperty =
|
|
||||||
DependencyPropertyHelper.Register<MapBase, MapProjection>(nameof(MapProjection), new WebMercatorProjection(),
|
|
||||||
(map, oldValue, newValue) => map.MapProjectionPropertyChanged(newValue));
|
|
||||||
|
|
||||||
private Location transformCenter;
|
|
||||||
private Point viewCenter;
|
|
||||||
private double centerLongitude;
|
|
||||||
private double maxLatitude = 85.05112878; // default WebMercatorProjection
|
|
||||||
private bool internalPropertyChange;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Raised when the current map viewport has changed.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<ViewportChangedEventArgs> ViewportChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the map foreground Brush.
|
|
||||||
/// </summary>
|
|
||||||
public Brush Foreground
|
|
||||||
{
|
{
|
||||||
get => (Brush)GetValue(ForegroundProperty);
|
SetTransformCenter(center);
|
||||||
set => SetValue(ForegroundProperty, value);
|
viewCenter = new Point(viewCenter.X + translation.X, viewCenter.Y + translation.Y);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
if (rotation != 0d)
|
||||||
/// Gets or sets the duration of the Center, ZoomLevel and Heading animations.
|
|
||||||
/// The default value is 0.3 seconds.
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan AnimationDuration
|
|
||||||
{
|
|
||||||
get => (TimeSpan)GetValue(AnimationDurationProperty);
|
|
||||||
set => SetValue(AnimationDurationProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the MapProjection used by the map control.
|
|
||||||
/// </summary>
|
|
||||||
public MapProjection MapProjection
|
|
||||||
{
|
|
||||||
get => (MapProjection)GetValue(MapProjectionProperty);
|
|
||||||
set => SetValue(MapProjectionProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the location of the center point of the map.
|
|
||||||
/// </summary>
|
|
||||||
public Location Center
|
|
||||||
{
|
|
||||||
get => (Location)GetValue(CenterProperty);
|
|
||||||
set => SetValue(CenterProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the target value of a Center animation.
|
|
||||||
/// </summary>
|
|
||||||
public Location TargetCenter
|
|
||||||
{
|
|
||||||
get => (Location)GetValue(TargetCenterProperty);
|
|
||||||
set => SetValue(TargetCenterProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the minimum value of the ZoomLevel and TargetZoomLevel properties.
|
|
||||||
/// Must not be less than zero or greater than MaxZoomLevel. The default value is 1.
|
|
||||||
/// </summary>
|
|
||||||
public double MinZoomLevel
|
|
||||||
{
|
|
||||||
get => (double)GetValue(MinZoomLevelProperty);
|
|
||||||
set => SetValue(MinZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the maximum value of the ZoomLevel and TargetZoomLevel properties.
|
|
||||||
/// Must not be less than MinZoomLevel. The default value is 20.
|
|
||||||
/// </summary>
|
|
||||||
public double MaxZoomLevel
|
|
||||||
{
|
|
||||||
get => (double)GetValue(MaxZoomLevelProperty);
|
|
||||||
set => SetValue(MaxZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the map zoom level.
|
|
||||||
/// </summary>
|
|
||||||
public double ZoomLevel
|
|
||||||
{
|
|
||||||
get => (double)GetValue(ZoomLevelProperty);
|
|
||||||
set => SetValue(ZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the target value of a ZoomLevel animation.
|
|
||||||
/// </summary>
|
|
||||||
public double TargetZoomLevel
|
|
||||||
{
|
|
||||||
get => (double)GetValue(TargetZoomLevelProperty);
|
|
||||||
set => SetValue(TargetZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the map heading, a counter-clockwise rotation angle in degrees.
|
|
||||||
/// </summary>
|
|
||||||
public double Heading
|
|
||||||
{
|
|
||||||
get => (double)GetValue(HeadingProperty);
|
|
||||||
set => SetValue(HeadingProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the target value of a Heading animation.
|
|
||||||
/// </summary>
|
|
||||||
public double TargetHeading
|
|
||||||
{
|
|
||||||
get => (double)GetValue(TargetHeadingProperty);
|
|
||||||
set => SetValue(TargetHeadingProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the ViewTransform instance that is used to transform between
|
|
||||||
/// projected map coordinates and view coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public ViewTransform ViewTransform { get; } = new ViewTransform();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a transform Matrix from meters to view coordinates for scaling and rotating
|
|
||||||
/// at the specified geographic coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public Matrix GetMapToViewTransform(double latitude, double longitude)
|
|
||||||
{
|
|
||||||
var transform = MapProjection.RelativeTransform(latitude, longitude);
|
|
||||||
transform.Scale(ViewTransform.Scale, ViewTransform.Scale);
|
|
||||||
transform.Rotate(ViewTransform.Rotation);
|
|
||||||
return transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a transform Matrix from meters to view coordinates for scaling and rotating
|
|
||||||
/// at the specified Location.
|
|
||||||
/// </summary>
|
|
||||||
public Matrix GetMapToViewTransform(Location location) => GetMapToViewTransform(location.Latitude, location.Longitude);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transforms geographic coordinates to a Point in view coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public Point LocationToView(double latitude, double longitude) => ViewTransform.MapToView(MapProjection.LocationToMap(latitude, longitude));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transforms a Location in geographic coordinates to a Point in view coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public Point LocationToView(Location location) => LocationToView(location.Latitude, location.Longitude);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transforms a Point in view coordinates to a Location in geographic coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public Location ViewToLocation(Point point) => MapProjection.MapToLocation(ViewTransform.ViewToMap(point));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets a temporary center point in view coordinates for scaling and rotation transformations.
|
|
||||||
/// This center point is automatically reset when the Center property is set by application code
|
|
||||||
/// or by the methods TranslateMap, TransformMap, ZoomMap and ZoomToBounds.
|
|
||||||
/// </summary>
|
|
||||||
public void SetTransformCenter(Point center)
|
|
||||||
{
|
|
||||||
viewCenter = center;
|
|
||||||
transformCenter = ViewToLocation(center);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resets the temporary transform center point set by SetTransformCenter.
|
|
||||||
/// </summary>
|
|
||||||
public void ResetTransformCenter()
|
|
||||||
{
|
|
||||||
viewCenter = new Point(ActualWidth / 2d, ActualHeight / 2d);
|
|
||||||
transformCenter = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Changes the Center property according to the specified translation in view coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public void TranslateMap(Point translation)
|
|
||||||
{
|
|
||||||
if (translation.X != 0d || translation.Y != 0d)
|
|
||||||
{
|
{
|
||||||
Center = ViewToLocation(new Point(viewCenter.X - translation.X, viewCenter.Y - translation.Y));
|
var heading = CoerceHeadingProperty(Heading - rotation);
|
||||||
|
SetValueInternal(HeadingProperty, heading);
|
||||||
|
SetValueInternal(TargetHeadingProperty, heading);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
if (scale != 1d)
|
||||||
/// Changes the Center, Heading and ZoomLevel properties according to the specified
|
|
||||||
/// view coordinate translation, rotation and scale delta values. Rotation and scaling
|
|
||||||
/// is performed relative to the specified center point in view coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public void TransformMap(Point center, Point translation, double rotation, double scale)
|
|
||||||
{
|
|
||||||
if (rotation == 0d && scale == 1d)
|
|
||||||
{
|
{
|
||||||
TranslateMap(translation);
|
var zoomLevel = CoerceZoomLevelProperty(ZoomLevel + Math.Log(scale, 2d));
|
||||||
|
SetValueInternal(ZoomLevelProperty, zoomLevel);
|
||||||
|
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
UpdateTransform(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the ZoomLevel or TargetZoomLevel property while retaining
|
||||||
|
/// the specified center point in view coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public void ZoomMap(Point center, double zoomLevel, bool animated = true)
|
||||||
|
{
|
||||||
|
zoomLevel = CoerceZoomLevelProperty(zoomLevel);
|
||||||
|
|
||||||
|
if (animated || zoomLevelAnimation != null)
|
||||||
|
{
|
||||||
|
if (TargetZoomLevel != zoomLevel)
|
||||||
{
|
{
|
||||||
SetTransformCenter(center);
|
SetTransformCenter(center);
|
||||||
viewCenter = new Point(viewCenter.X + translation.X, viewCenter.Y + translation.Y);
|
TargetZoomLevel = zoomLevel;
|
||||||
|
}
|
||||||
if (rotation != 0d)
|
}
|
||||||
{
|
else
|
||||||
var heading = CoerceHeadingProperty(Heading - rotation);
|
{
|
||||||
SetValueInternal(HeadingProperty, heading);
|
if (ZoomLevel != zoomLevel)
|
||||||
SetValueInternal(TargetHeadingProperty, heading);
|
{
|
||||||
}
|
SetTransformCenter(center);
|
||||||
|
SetValueInternal(ZoomLevelProperty, zoomLevel);
|
||||||
if (scale != 1d)
|
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
||||||
{
|
|
||||||
var zoomLevel = CoerceZoomLevelProperty(ZoomLevel + Math.Log(scale, 2d));
|
|
||||||
SetValueInternal(ZoomLevelProperty, zoomLevel);
|
|
||||||
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateTransform(true);
|
UpdateTransform(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the ZoomLevel or TargetZoomLevel property while retaining
|
/// Sets the TargetZoomLevel and TargetCenter properties so that the specified BoundingBox
|
||||||
/// the specified center point in view coordinates.
|
/// fits into the current view. The TargetHeading property is set to zero.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ZoomMap(Point center, double zoomLevel, bool animated = true)
|
public void ZoomToBounds(BoundingBox bounds)
|
||||||
|
{
|
||||||
|
(var rect, var _) = MapProjection.BoundingBoxToMap(bounds);
|
||||||
|
var scale = Math.Min(ActualWidth / rect.Width, ActualHeight / rect.Height);
|
||||||
|
TargetZoomLevel = ScaleToZoomLevel(scale);
|
||||||
|
TargetCenter = new Location((bounds.South + bounds.North) / 2d, (bounds.West + bounds.East) / 2d);
|
||||||
|
TargetHeading = 0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool InsideViewBounds(Point point)
|
||||||
|
{
|
||||||
|
return point.X >= 0d && point.Y >= 0d && point.X <= ActualWidth && point.Y <= ActualHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal double NearestLongitude(double longitude)
|
||||||
|
{
|
||||||
|
longitude = Location.NormalizeLongitude(longitude);
|
||||||
|
|
||||||
|
var offset = longitude - Center.Longitude;
|
||||||
|
|
||||||
|
if (offset > 180d)
|
||||||
{
|
{
|
||||||
zoomLevel = CoerceZoomLevelProperty(zoomLevel);
|
longitude = Center.Longitude + (offset % 360d) - 360d;
|
||||||
|
}
|
||||||
|
else if (offset < -180d)
|
||||||
|
{
|
||||||
|
longitude = Center.Longitude + (offset % 360d) + 360d;
|
||||||
|
}
|
||||||
|
|
||||||
if (animated || zoomLevelAnimation != null)
|
return longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location CoerceCenterProperty(Location center)
|
||||||
|
{
|
||||||
|
if (center == null)
|
||||||
|
{
|
||||||
|
center = new Location(0d, 0d);
|
||||||
|
}
|
||||||
|
else if (center.Latitude < -maxLatitude || center.Latitude > maxLatitude ||
|
||||||
|
center.Longitude < -180d || center.Longitude > 180d)
|
||||||
|
{
|
||||||
|
center = new Location(
|
||||||
|
Math.Min(Math.Max(center.Latitude, -maxLatitude), maxLatitude),
|
||||||
|
Location.NormalizeLongitude(center.Longitude));
|
||||||
|
}
|
||||||
|
|
||||||
|
return center;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CoerceMinZoomLevelProperty(double minZoomLevel)
|
||||||
|
{
|
||||||
|
return Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CoerceMaxZoomLevelProperty(double maxZoomLevel)
|
||||||
|
{
|
||||||
|
return Math.Max(maxZoomLevel, MinZoomLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CoerceZoomLevelProperty(double zoomLevel)
|
||||||
|
{
|
||||||
|
return Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CoerceHeadingProperty(double heading)
|
||||||
|
{
|
||||||
|
return ((heading % 360d) + 360d) % 360d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetValueInternal(DependencyProperty property, object value)
|
||||||
|
{
|
||||||
|
internalPropertyChange = true;
|
||||||
|
SetValue(property, value);
|
||||||
|
internalPropertyChange = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MapProjectionPropertyChanged(MapProjection projection)
|
||||||
|
{
|
||||||
|
maxLatitude = 90d;
|
||||||
|
|
||||||
|
if (projection.IsNormalCylindrical)
|
||||||
|
{
|
||||||
|
maxLatitude = projection.MapToLocation(0d, 180d * MapProjection.Wgs84MeterPerDegree).Latitude;
|
||||||
|
Center = CoerceCenterProperty(Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetTransformCenter();
|
||||||
|
UpdateTransform(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTransform(bool resetTransformCenter = false, bool projectionChanged = false)
|
||||||
|
{
|
||||||
|
var transformCenterChanged = false;
|
||||||
|
var viewScale = ZoomLevelToScale(ZoomLevel);
|
||||||
|
var mapCenter = MapProjection.LocationToMap(transformCenter ?? Center);
|
||||||
|
|
||||||
|
ViewTransform.SetTransform(mapCenter, viewCenter, viewScale, -Heading);
|
||||||
|
|
||||||
|
if (transformCenter != null)
|
||||||
|
{
|
||||||
|
var center = ViewToLocation(new Point(ActualWidth / 2d, ActualHeight / 2d));
|
||||||
|
var latitude = center.Latitude;
|
||||||
|
var longitude = Location.NormalizeLongitude(center.Longitude);
|
||||||
|
|
||||||
|
if (latitude < -maxLatitude || latitude > maxLatitude)
|
||||||
{
|
{
|
||||||
if (TargetZoomLevel != zoomLevel)
|
latitude = Math.Min(Math.Max(latitude, -maxLatitude), maxLatitude);
|
||||||
{
|
resetTransformCenter = true;
|
||||||
SetTransformCenter(center);
|
|
||||||
TargetZoomLevel = zoomLevel;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (!center.Equals(latitude, longitude))
|
||||||
{
|
{
|
||||||
if (ZoomLevel != zoomLevel)
|
center = new Location(latitude, longitude);
|
||||||
{
|
}
|
||||||
SetTransformCenter(center);
|
|
||||||
SetValueInternal(ZoomLevelProperty, zoomLevel);
|
SetValueInternal(CenterProperty, center);
|
||||||
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
|
||||||
UpdateTransform(true);
|
if (centerAnimation == null)
|
||||||
}
|
{
|
||||||
|
SetValueInternal(TargetCenterProperty, center);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetTransformCenter)
|
||||||
|
{
|
||||||
|
// Check if transform center has moved across 180° longitude.
|
||||||
|
//
|
||||||
|
transformCenterChanged = Math.Abs(center.Longitude - transformCenter.Longitude) > 180d;
|
||||||
|
ResetTransformCenter();
|
||||||
|
mapCenter = MapProjection.LocationToMap(center);
|
||||||
|
ViewTransform.SetTransform(mapCenter, viewCenter, viewScale, -Heading);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
ViewScale = ViewTransform.Scale;
|
||||||
/// Sets the TargetZoomLevel and TargetCenter properties so that the specified BoundingBox
|
|
||||||
/// fits into the current view. The TargetHeading property is set to zero.
|
|
||||||
/// </summary>
|
|
||||||
public void ZoomToBounds(BoundingBox bounds)
|
|
||||||
{
|
|
||||||
(var rect, var _) = MapProjection.BoundingBoxToMap(bounds);
|
|
||||||
var scale = Math.Min(ActualWidth / rect.Width, ActualHeight / rect.Height);
|
|
||||||
TargetZoomLevel = ScaleToZoomLevel(scale);
|
|
||||||
TargetCenter = new Location((bounds.South + bounds.North) / 2d, (bounds.West + bounds.East) / 2d);
|
|
||||||
TargetHeading = 0d;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool InsideViewBounds(Point point)
|
// Check if view center has moved across 180° longitude.
|
||||||
{
|
//
|
||||||
return point.X >= 0d && point.Y >= 0d && point.X <= ActualWidth && point.Y <= ActualHeight;
|
transformCenterChanged = transformCenterChanged || Math.Abs(Center.Longitude - centerLongitude) > 180d;
|
||||||
}
|
centerLongitude = Center.Longitude;
|
||||||
|
|
||||||
internal double NearestLongitude(double longitude)
|
OnViewportChanged(new ViewportChangedEventArgs(projectionChanged, transformCenterChanged));
|
||||||
{
|
}
|
||||||
longitude = Location.NormalizeLongitude(longitude);
|
|
||||||
|
|
||||||
var offset = longitude - Center.Longitude;
|
|
||||||
|
|
||||||
if (offset > 180d)
|
protected override void OnViewportChanged(ViewportChangedEventArgs e)
|
||||||
{
|
{
|
||||||
longitude = Center.Longitude + (offset % 360d) - 360d;
|
base.OnViewportChanged(e);
|
||||||
}
|
ViewportChanged?.Invoke(this, e);
|
||||||
else if (offset < -180d)
|
|
||||||
{
|
|
||||||
longitude = Center.Longitude + (offset % 360d) + 360d;
|
|
||||||
}
|
|
||||||
|
|
||||||
return longitude;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Location CoerceCenterProperty(Location center)
|
|
||||||
{
|
|
||||||
if (center == null)
|
|
||||||
{
|
|
||||||
center = new Location(0d, 0d);
|
|
||||||
}
|
|
||||||
else if (center.Latitude < -maxLatitude || center.Latitude > maxLatitude ||
|
|
||||||
center.Longitude < -180d || center.Longitude > 180d)
|
|
||||||
{
|
|
||||||
center = new Location(
|
|
||||||
Math.Min(Math.Max(center.Latitude, -maxLatitude), maxLatitude),
|
|
||||||
Location.NormalizeLongitude(center.Longitude));
|
|
||||||
}
|
|
||||||
|
|
||||||
return center;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double CoerceMinZoomLevelProperty(double minZoomLevel)
|
|
||||||
{
|
|
||||||
return Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double CoerceMaxZoomLevelProperty(double maxZoomLevel)
|
|
||||||
{
|
|
||||||
return Math.Max(maxZoomLevel, MinZoomLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double CoerceZoomLevelProperty(double zoomLevel)
|
|
||||||
{
|
|
||||||
return Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double CoerceHeadingProperty(double heading)
|
|
||||||
{
|
|
||||||
return ((heading % 360d) + 360d) % 360d;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetValueInternal(DependencyProperty property, object value)
|
|
||||||
{
|
|
||||||
internalPropertyChange = true;
|
|
||||||
SetValue(property, value);
|
|
||||||
internalPropertyChange = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MapProjectionPropertyChanged(MapProjection projection)
|
|
||||||
{
|
|
||||||
maxLatitude = 90d;
|
|
||||||
|
|
||||||
if (projection.IsNormalCylindrical)
|
|
||||||
{
|
|
||||||
maxLatitude = projection.MapToLocation(0d, 180d * MapProjection.Wgs84MeterPerDegree).Latitude;
|
|
||||||
Center = CoerceCenterProperty(Center);
|
|
||||||
}
|
|
||||||
|
|
||||||
ResetTransformCenter();
|
|
||||||
UpdateTransform(false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateTransform(bool resetTransformCenter = false, bool projectionChanged = false)
|
|
||||||
{
|
|
||||||
var transformCenterChanged = false;
|
|
||||||
var viewScale = ZoomLevelToScale(ZoomLevel);
|
|
||||||
var mapCenter = MapProjection.LocationToMap(transformCenter ?? Center);
|
|
||||||
|
|
||||||
ViewTransform.SetTransform(mapCenter, viewCenter, viewScale, -Heading);
|
|
||||||
|
|
||||||
if (transformCenter != null)
|
|
||||||
{
|
|
||||||
var center = ViewToLocation(new Point(ActualWidth / 2d, ActualHeight / 2d));
|
|
||||||
var latitude = center.Latitude;
|
|
||||||
var longitude = Location.NormalizeLongitude(center.Longitude);
|
|
||||||
|
|
||||||
if (latitude < -maxLatitude || latitude > maxLatitude)
|
|
||||||
{
|
|
||||||
latitude = Math.Min(Math.Max(latitude, -maxLatitude), maxLatitude);
|
|
||||||
resetTransformCenter = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!center.Equals(latitude, longitude))
|
|
||||||
{
|
|
||||||
center = new Location(latitude, longitude);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetValueInternal(CenterProperty, center);
|
|
||||||
|
|
||||||
if (centerAnimation == null)
|
|
||||||
{
|
|
||||||
SetValueInternal(TargetCenterProperty, center);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resetTransformCenter)
|
|
||||||
{
|
|
||||||
// Check if transform center has moved across 180° longitude.
|
|
||||||
//
|
|
||||||
transformCenterChanged = Math.Abs(center.Longitude - transformCenter.Longitude) > 180d;
|
|
||||||
ResetTransformCenter();
|
|
||||||
mapCenter = MapProjection.LocationToMap(center);
|
|
||||||
ViewTransform.SetTransform(mapCenter, viewCenter, viewScale, -Heading);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewScale = ViewTransform.Scale;
|
|
||||||
|
|
||||||
// Check if view center has moved across 180° longitude.
|
|
||||||
//
|
|
||||||
transformCenterChanged = transformCenterChanged || Math.Abs(Center.Longitude - centerLongitude) > 180d;
|
|
||||||
centerLongitude = Center.Longitude;
|
|
||||||
|
|
||||||
OnViewportChanged(new ViewportChangedEventArgs(projectionChanged, transformCenterChanged));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnViewportChanged(ViewportChangedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnViewportChanged(e);
|
|
||||||
ViewportChanged?.Invoke(this, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,84 +8,83 @@ using Microsoft.UI.Xaml;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A MapPanel that adjusts the ViewPosition property of its child elements so that
|
||||||
|
/// elements that would be outside the current viewport are arranged on a border area.
|
||||||
|
/// Such elements are arranged at a distance of BorderWidth/2 from the edges of the
|
||||||
|
/// MapBorderPanel in direction of their original azimuth from the map center.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapBorderPanel : MapPanel
|
||||||
{
|
{
|
||||||
/// <summary>
|
public static readonly DependencyProperty BorderWidthProperty =
|
||||||
/// A MapPanel that adjusts the ViewPosition property of its child elements so that
|
DependencyPropertyHelper.Register<MapBorderPanel, double>(nameof(BorderWidth));
|
||||||
/// elements that would be outside the current viewport are arranged on a border area.
|
|
||||||
/// Such elements are arranged at a distance of BorderWidth/2 from the edges of the
|
public static readonly DependencyProperty OnBorderProperty =
|
||||||
/// MapBorderPanel in direction of their original azimuth from the map center.
|
DependencyPropertyHelper.RegisterAttached<bool>("OnBorder", typeof(MapBorderPanel));
|
||||||
/// </summary>
|
|
||||||
public partial class MapBorderPanel : MapPanel
|
public double BorderWidth
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty BorderWidthProperty =
|
get => (double)GetValue(BorderWidthProperty);
|
||||||
DependencyPropertyHelper.Register<MapBorderPanel, double>(nameof(BorderWidth));
|
set => SetValue(BorderWidthProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty OnBorderProperty =
|
public static bool GetOnBorder(FrameworkElement element)
|
||||||
DependencyPropertyHelper.RegisterAttached<bool>("OnBorder", typeof(MapBorderPanel));
|
{
|
||||||
|
return (bool)element.GetValue(OnBorderProperty);
|
||||||
|
}
|
||||||
|
|
||||||
public double BorderWidth
|
protected override Point SetViewPosition(FrameworkElement element, Point position)
|
||||||
|
{
|
||||||
|
var onBorder = false;
|
||||||
|
var w = ParentMap.ActualWidth;
|
||||||
|
var h = ParentMap.ActualHeight;
|
||||||
|
var minX = BorderWidth / 2d;
|
||||||
|
var minY = BorderWidth / 2d;
|
||||||
|
var maxX = w - BorderWidth / 2d;
|
||||||
|
var maxY = h - BorderWidth / 2d;
|
||||||
|
|
||||||
|
if (position.X < minX || position.X > maxX ||
|
||||||
|
position.Y < minY || position.Y > maxY)
|
||||||
{
|
{
|
||||||
get => (double)GetValue(BorderWidthProperty);
|
var dx = position.X - w / 2d;
|
||||||
set => SetValue(BorderWidthProperty, value);
|
var dy = position.Y - h / 2d;
|
||||||
}
|
var cx = (maxX - minX) / 2d;
|
||||||
|
var cy = (maxY - minY) / 2d;
|
||||||
|
double x, y;
|
||||||
|
|
||||||
public static bool GetOnBorder(FrameworkElement element)
|
if (dx < 0d)
|
||||||
{
|
|
||||||
return (bool)element.GetValue(OnBorderProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Point SetViewPosition(FrameworkElement element, Point position)
|
|
||||||
{
|
|
||||||
var onBorder = false;
|
|
||||||
var w = ParentMap.ActualWidth;
|
|
||||||
var h = ParentMap.ActualHeight;
|
|
||||||
var minX = BorderWidth / 2d;
|
|
||||||
var minY = BorderWidth / 2d;
|
|
||||||
var maxX = w - BorderWidth / 2d;
|
|
||||||
var maxY = h - BorderWidth / 2d;
|
|
||||||
|
|
||||||
if (position.X < minX || position.X > maxX ||
|
|
||||||
position.Y < minY || position.Y > maxY)
|
|
||||||
{
|
{
|
||||||
var dx = position.X - w / 2d;
|
x = minX;
|
||||||
var dy = position.Y - h / 2d;
|
y = minY + cy - cx * dy / dx;
|
||||||
var cx = (maxX - minX) / 2d;
|
}
|
||||||
var cy = (maxY - minY) / 2d;
|
else
|
||||||
double x, y;
|
{
|
||||||
|
x = maxX;
|
||||||
|
y = minY + cy + cx * dy / dx;
|
||||||
|
}
|
||||||
|
|
||||||
if (dx < 0d)
|
if (y < minY || y > maxY)
|
||||||
|
{
|
||||||
|
if (dy < 0d)
|
||||||
{
|
{
|
||||||
x = minX;
|
x = minX + cx - cy * dx / dy;
|
||||||
y = minY + cy - cx * dy / dx;
|
y = minY;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
x = maxX;
|
x = minX + cx + cy * dx / dy;
|
||||||
y = minY + cy + cx * dy / dx;
|
y = maxY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (y < minY || y > maxY)
|
|
||||||
{
|
|
||||||
if (dy < 0d)
|
|
||||||
{
|
|
||||||
x = minX + cx - cy * dx / dy;
|
|
||||||
y = minY;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x = minX + cx + cy * dx / dy;
|
|
||||||
y = maxY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
position = new Point(x, y);
|
|
||||||
onBorder = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
element.SetValue(OnBorderProperty, onBorder);
|
position = new Point(x, y);
|
||||||
|
onBorder = true;
|
||||||
return base.SetViewPosition(element, position);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
element.SetValue(OnBorderProperty, onBorder);
|
||||||
|
|
||||||
|
return base.SetViewPosition(element, position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,44 +11,43 @@ using Microsoft.UI.Xaml.Controls;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ContentControl placed on a MapPanel at a geographic location specified by the Location property.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapContentControl : ContentControl
|
||||||
{
|
{
|
||||||
|
public static readonly DependencyProperty LocationProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapContentControl, Location>(
|
||||||
|
nameof(Location), MapPanel.LocationProperty);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty AutoCollapseProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapContentControl, bool>(
|
||||||
|
nameof(AutoCollapse), MapPanel.AutoCollapseProperty);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ContentControl placed on a MapPanel at a geographic location specified by the Location property.
|
/// Gets/sets MapPanel.Location.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MapContentControl : ContentControl
|
public Location Location
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty LocationProperty =
|
get => (Location)GetValue(LocationProperty);
|
||||||
DependencyPropertyHelper.AddOwner<MapContentControl, Location>(
|
set => SetValue(LocationProperty, value);
|
||||||
nameof(Location), MapPanel.LocationProperty);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty AutoCollapseProperty =
|
|
||||||
DependencyPropertyHelper.AddOwner<MapContentControl, bool>(
|
|
||||||
nameof(AutoCollapse), MapPanel.AutoCollapseProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets/sets MapPanel.Location.
|
|
||||||
/// </summary>
|
|
||||||
public Location Location
|
|
||||||
{
|
|
||||||
get => (Location)GetValue(LocationProperty);
|
|
||||||
set => SetValue(LocationProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets/sets MapPanel.AutoCollapse.
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoCollapse
|
|
||||||
{
|
|
||||||
get => (bool)GetValue(AutoCollapseProperty);
|
|
||||||
set => SetValue(AutoCollapseProperty, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MapContentControl with a Pushpin Style.
|
/// Gets/sets MapPanel.AutoCollapse.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class Pushpin : MapContentControl
|
public bool AutoCollapse
|
||||||
{
|
{
|
||||||
|
get => (bool)GetValue(AutoCollapseProperty);
|
||||||
|
set => SetValue(AutoCollapseProperty, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MapContentControl with a Pushpin Style.
|
||||||
|
/// </summary>
|
||||||
|
public partial class Pushpin : MapContentControl
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,254 +17,253 @@ using Avalonia.Layout;
|
||||||
using PathFigureCollection = Avalonia.Media.PathFigures;
|
using PathFigureCollection = Avalonia.Media.PathFigures;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws a map graticule, i.e. a lat/lon grid overlay.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapGraticule : MapGrid
|
||||||
{
|
{
|
||||||
/// <summary>
|
protected override void DrawGrid(PathFigureCollection figures, List<Label> labels)
|
||||||
/// Draws a map graticule, i.e. a lat/lon grid overlay.
|
|
||||||
/// </summary>
|
|
||||||
public partial class MapGraticule : MapGrid
|
|
||||||
{
|
{
|
||||||
protected override void DrawGrid(PathFigureCollection figures, List<Label> labels)
|
if (ParentMap.MapProjection.IsNormalCylindrical)
|
||||||
{
|
{
|
||||||
if (ParentMap.MapProjection.IsNormalCylindrical)
|
DrawNormalGraticule(figures, labels);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DrawGraticule(figures, labels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly double[] lineDistances = [
|
||||||
|
1d/3600d, 1d/1800d, 1d/720d, 1d/360d, 1d/240d, 1d/120d,
|
||||||
|
1d/60d, 1d/30d, 1d/12d, 1d/6d, 1d/4d, 1d/2d,
|
||||||
|
1d, 2d, 5d, 10d, 15d, 30d];
|
||||||
|
|
||||||
|
private static string GetLabelFormat(double lineDistance)
|
||||||
|
{
|
||||||
|
return lineDistance < 1d / 60d ? "{0} {1}°{2:00}'{3:00}\"" :
|
||||||
|
lineDistance < 1d ? "{0} {1}°{2:00}'" : "{0} {1}°";
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetLineDistance(bool scaleByLatitude)
|
||||||
|
{
|
||||||
|
var minDistance = MinLineDistance / (ParentMap.ViewTransform.Scale * MapProjection.Wgs84MeterPerDegree);
|
||||||
|
|
||||||
|
if (scaleByLatitude)
|
||||||
|
{
|
||||||
|
minDistance /= Math.Cos(ParentMap.Center.Latitude * Math.PI / 180d);
|
||||||
|
}
|
||||||
|
|
||||||
|
minDistance = Math.Max(minDistance, lineDistances.First());
|
||||||
|
minDistance = Math.Min(minDistance, lineDistances.Last());
|
||||||
|
|
||||||
|
return lineDistances.First(d => d >= minDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNormalGraticule(PathFigureCollection figures, List<Label> labels)
|
||||||
|
{
|
||||||
|
var lineDistance = GetLineDistance(false);
|
||||||
|
var labelFormat = GetLabelFormat(lineDistance);
|
||||||
|
var mapRect = ParentMap.ViewTransform.ViewToMapBounds(new Rect(0d, 0d, ParentMap.ActualWidth, ParentMap.ActualHeight));
|
||||||
|
var southWest = ParentMap.MapProjection.MapToLocation(mapRect.X, mapRect.Y);
|
||||||
|
var northEast = ParentMap.MapProjection.MapToLocation(mapRect.X + mapRect.Width, mapRect.Y + mapRect.Height);
|
||||||
|
var minLat = Math.Ceiling(southWest.Latitude / lineDistance) * lineDistance;
|
||||||
|
var minLon = Math.Ceiling(southWest.Longitude / lineDistance) * lineDistance;
|
||||||
|
|
||||||
|
for (var lat = minLat; lat <= northEast.Latitude; lat += lineDistance)
|
||||||
|
{
|
||||||
|
var p1 = ParentMap.LocationToView(lat, southWest.Longitude);
|
||||||
|
var p2 = ParentMap.LocationToView(lat, northEast.Longitude);
|
||||||
|
figures.Add(CreateLineFigure(p1, p2));
|
||||||
|
|
||||||
|
if (ParentMap.ViewTransform.Rotation == 0d)
|
||||||
{
|
{
|
||||||
DrawNormalGraticule(figures, labels);
|
var text = GetLatitudeLabelText(lat, labelFormat);
|
||||||
|
labels.Add(new Label(text, p1.X, p1.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Bottom));
|
||||||
|
labels.Add(new Label(text, p2.X, p2.Y, 0d, HorizontalAlignment.Right, VerticalAlignment.Bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var lon = minLon; lon <= northEast.Longitude; lon += lineDistance)
|
||||||
|
{
|
||||||
|
var p1 = ParentMap.LocationToView(southWest.Latitude, lon);
|
||||||
|
var p2 = ParentMap.LocationToView(northEast.Latitude, lon);
|
||||||
|
figures.Add(CreateLineFigure(p1, p2));
|
||||||
|
|
||||||
|
if (ParentMap.ViewTransform.Rotation == 0d)
|
||||||
|
{
|
||||||
|
var text = GetLongitudeLabelText(lon, labelFormat);
|
||||||
|
labels.Add(new Label(text, p1.X, p1.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Bottom));
|
||||||
|
labels.Add(new Label(text, p2.X, p2.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Top));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DrawGraticule(figures, labels);
|
for (var lat = minLat; lat <= northEast.Latitude; lat += lineDistance)
|
||||||
|
{
|
||||||
|
AddLabel(labels, labelFormat, lat, lon, ParentMap.LocationToView(lat, lon), 0d);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly double[] lineDistances = [
|
private void DrawGraticule(PathFigureCollection figures, List<Label> labels)
|
||||||
1d/3600d, 1d/1800d, 1d/720d, 1d/360d, 1d/240d, 1d/120d,
|
{
|
||||||
1d/60d, 1d/30d, 1d/12d, 1d/6d, 1d/4d, 1d/2d,
|
var lineDistance = GetLineDistance(true);
|
||||||
1d, 2d, 5d, 10d, 15d, 30d];
|
var labelFormat = GetLabelFormat(lineDistance);
|
||||||
|
var pointDistance = Math.Min(lineDistance, 1d);
|
||||||
|
var interpolationCount = (int)(lineDistance / pointDistance);
|
||||||
|
var center = Math.Round(ParentMap.Center.Longitude / lineDistance) * lineDistance;
|
||||||
|
var minLat = Math.Round(ParentMap.Center.Latitude / pointDistance) * pointDistance;
|
||||||
|
var maxLat = minLat;
|
||||||
|
var minLon = center;
|
||||||
|
var maxLon = center;
|
||||||
|
|
||||||
private static string GetLabelFormat(double lineDistance)
|
for (var lon = center;
|
||||||
|
lon >= center - 180d && DrawMeridian(figures, lon, pointDistance, ref minLat, ref maxLat);
|
||||||
|
lon -= lineDistance)
|
||||||
{
|
{
|
||||||
return lineDistance < 1d / 60d ? "{0} {1}°{2:00}'{3:00}\"" :
|
minLon = lon;
|
||||||
lineDistance < 1d ? "{0} {1}°{2:00}'" : "{0} {1}°";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double GetLineDistance(bool scaleByLatitude)
|
for (var lon = center + lineDistance;
|
||||||
|
lon < center + 180d && DrawMeridian(figures, lon, pointDistance, ref minLat, ref maxLat);
|
||||||
|
lon += lineDistance)
|
||||||
{
|
{
|
||||||
var minDistance = MinLineDistance / (ParentMap.ViewTransform.Scale * MapProjection.Wgs84MeterPerDegree);
|
maxLon = lon;
|
||||||
|
|
||||||
if (scaleByLatitude)
|
|
||||||
{
|
|
||||||
minDistance /= Math.Cos(ParentMap.Center.Latitude * Math.PI / 180d);
|
|
||||||
}
|
|
||||||
|
|
||||||
minDistance = Math.Max(minDistance, lineDistances.First());
|
|
||||||
minDistance = Math.Min(minDistance, lineDistances.Last());
|
|
||||||
|
|
||||||
return lineDistances.First(d => d >= minDistance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawNormalGraticule(PathFigureCollection figures, List<Label> labels)
|
if (minLon + 360d > maxLon)
|
||||||
{
|
{
|
||||||
var lineDistance = GetLineDistance(false);
|
minLon -= lineDistance;
|
||||||
var labelFormat = GetLabelFormat(lineDistance);
|
|
||||||
var mapRect = ParentMap.ViewTransform.ViewToMapBounds(new Rect(0d, 0d, ParentMap.ActualWidth, ParentMap.ActualHeight));
|
|
||||||
var southWest = ParentMap.MapProjection.MapToLocation(mapRect.X, mapRect.Y);
|
|
||||||
var northEast = ParentMap.MapProjection.MapToLocation(mapRect.X + mapRect.Width, mapRect.Y + mapRect.Height);
|
|
||||||
var minLat = Math.Ceiling(southWest.Latitude / lineDistance) * lineDistance;
|
|
||||||
var minLon = Math.Ceiling(southWest.Longitude / lineDistance) * lineDistance;
|
|
||||||
|
|
||||||
for (var lat = minLat; lat <= northEast.Latitude; lat += lineDistance)
|
|
||||||
{
|
|
||||||
var p1 = ParentMap.LocationToView(lat, southWest.Longitude);
|
|
||||||
var p2 = ParentMap.LocationToView(lat, northEast.Longitude);
|
|
||||||
figures.Add(CreateLineFigure(p1, p2));
|
|
||||||
|
|
||||||
if (ParentMap.ViewTransform.Rotation == 0d)
|
|
||||||
{
|
|
||||||
var text = GetLatitudeLabelText(lat, labelFormat);
|
|
||||||
labels.Add(new Label(text, p1.X, p1.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Bottom));
|
|
||||||
labels.Add(new Label(text, p2.X, p2.Y, 0d, HorizontalAlignment.Right, VerticalAlignment.Bottom));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var lon = minLon; lon <= northEast.Longitude; lon += lineDistance)
|
|
||||||
{
|
|
||||||
var p1 = ParentMap.LocationToView(southWest.Latitude, lon);
|
|
||||||
var p2 = ParentMap.LocationToView(northEast.Latitude, lon);
|
|
||||||
figures.Add(CreateLineFigure(p1, p2));
|
|
||||||
|
|
||||||
if (ParentMap.ViewTransform.Rotation == 0d)
|
|
||||||
{
|
|
||||||
var text = GetLongitudeLabelText(lon, labelFormat);
|
|
||||||
labels.Add(new Label(text, p1.X, p1.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Bottom));
|
|
||||||
labels.Add(new Label(text, p2.X, p2.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Top));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (var lat = minLat; lat <= northEast.Latitude; lat += lineDistance)
|
|
||||||
{
|
|
||||||
AddLabel(labels, labelFormat, lat, lon, ParentMap.LocationToView(lat, lon), 0d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawGraticule(PathFigureCollection figures, List<Label> labels)
|
if (maxLon - 360d < minLon)
|
||||||
{
|
{
|
||||||
var lineDistance = GetLineDistance(true);
|
maxLon += lineDistance;
|
||||||
var labelFormat = GetLabelFormat(lineDistance);
|
|
||||||
var pointDistance = Math.Min(lineDistance, 1d);
|
|
||||||
var interpolationCount = (int)(lineDistance / pointDistance);
|
|
||||||
var center = Math.Round(ParentMap.Center.Longitude / lineDistance) * lineDistance;
|
|
||||||
var minLat = Math.Round(ParentMap.Center.Latitude / pointDistance) * pointDistance;
|
|
||||||
var maxLat = minLat;
|
|
||||||
var minLon = center;
|
|
||||||
var maxLon = center;
|
|
||||||
|
|
||||||
for (var lon = center;
|
|
||||||
lon >= center - 180d && DrawMeridian(figures, lon, pointDistance, ref minLat, ref maxLat);
|
|
||||||
lon -= lineDistance)
|
|
||||||
{
|
|
||||||
minLon = lon;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var lon = center + lineDistance;
|
|
||||||
lon < center + 180d && DrawMeridian(figures, lon, pointDistance, ref minLat, ref maxLat);
|
|
||||||
lon += lineDistance)
|
|
||||||
{
|
|
||||||
maxLon = lon;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minLon + 360d > maxLon)
|
|
||||||
{
|
|
||||||
minLon -= lineDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxLon - 360d < minLon)
|
|
||||||
{
|
|
||||||
maxLon += lineDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pointDistance < lineDistance)
|
|
||||||
{
|
|
||||||
minLat = Math.Ceiling(minLat / lineDistance - 1e-6) * lineDistance;
|
|
||||||
maxLat = Math.Floor(maxLat / lineDistance + 1e-6) * lineDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
maxLat += 1e-6;
|
|
||||||
maxLon += 1e-6;
|
|
||||||
|
|
||||||
for (var lat = minLat; lat <= maxLat; lat += lineDistance)
|
|
||||||
{
|
|
||||||
var points = new List<Point>();
|
|
||||||
|
|
||||||
for (var lon = minLon; lon <= maxLon; lon += lineDistance)
|
|
||||||
{
|
|
||||||
var p = ParentMap.LocationToView(lat, lon);
|
|
||||||
points.Add(p);
|
|
||||||
AddLabel(labels, labelFormat, lat, lon, p, -ParentMap.MapProjection.GridConvergence(lat, lon));
|
|
||||||
|
|
||||||
for (int j = 1; j < interpolationCount; j++)
|
|
||||||
{
|
|
||||||
points.Add(ParentMap.LocationToView(lat, lon + j * pointDistance));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (points.Count >= 2)
|
|
||||||
{
|
|
||||||
figures.Add(CreatePolylineFigure(points));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool DrawMeridian(PathFigureCollection figures, double lon,
|
if (pointDistance < lineDistance)
|
||||||
double latStep, ref double minLat, ref double maxLat)
|
{
|
||||||
|
minLat = Math.Ceiling(minLat / lineDistance - 1e-6) * lineDistance;
|
||||||
|
maxLat = Math.Floor(maxLat / lineDistance + 1e-6) * lineDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxLat += 1e-6;
|
||||||
|
maxLon += 1e-6;
|
||||||
|
|
||||||
|
for (var lat = minLat; lat <= maxLat; lat += lineDistance)
|
||||||
{
|
{
|
||||||
var points = new List<Point>();
|
var points = new List<Point>();
|
||||||
var visible = false;
|
|
||||||
|
|
||||||
for (var lat = minLat + latStep; lat < maxLat; lat += latStep)
|
for (var lon = minLon; lon <= maxLon; lon += lineDistance)
|
||||||
{
|
{
|
||||||
var p = ParentMap.LocationToView(lat, lon);
|
var p = ParentMap.LocationToView(lat, lon);
|
||||||
points.Add(p);
|
points.Add(p);
|
||||||
visible = visible || ParentMap.InsideViewBounds(p);
|
AddLabel(labels, labelFormat, lat, lon, p, -ParentMap.MapProjection.GridConvergence(lat, lon));
|
||||||
|
|
||||||
|
for (int j = 1; j < interpolationCount; j++)
|
||||||
|
{
|
||||||
|
points.Add(ParentMap.LocationToView(lat, lon + j * pointDistance));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var lat = minLat; lat >= -90d; lat -= latStep)
|
if (points.Count >= 2)
|
||||||
{
|
|
||||||
var p = ParentMap.LocationToView(lat, lon);
|
|
||||||
points.Insert(0, p);
|
|
||||||
|
|
||||||
if (!ParentMap.InsideViewBounds(p)) break;
|
|
||||||
|
|
||||||
minLat = lat;
|
|
||||||
visible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var lat = maxLat; lat <= 90d; lat += latStep)
|
|
||||||
{
|
|
||||||
var p = ParentMap.LocationToView(lat, lon);
|
|
||||||
points.Add(p);
|
|
||||||
|
|
||||||
if (!ParentMap.InsideViewBounds(p)) break;
|
|
||||||
|
|
||||||
maxLat = lat;
|
|
||||||
visible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visible && points.Count >= 2)
|
|
||||||
{
|
{
|
||||||
figures.Add(CreatePolylineFigure(points));
|
figures.Add(CreatePolylineFigure(points));
|
||||||
}
|
}
|
||||||
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddLabel(List<Label> labels, string labelFormat, double lat, double lon, Point position, double rotation)
|
|
||||||
{
|
|
||||||
if (lat > -90d && lat < 90d && ParentMap.InsideViewBounds(position))
|
|
||||||
{
|
|
||||||
rotation = ((rotation + ParentMap.ViewTransform.Rotation) % 360d + 540d) % 360d - 180d;
|
|
||||||
|
|
||||||
if (rotation < -90d)
|
|
||||||
{
|
|
||||||
rotation += 180d;
|
|
||||||
}
|
|
||||||
else if (rotation > 90d)
|
|
||||||
{
|
|
||||||
rotation -= 180d;
|
|
||||||
}
|
|
||||||
|
|
||||||
var text = GetLatitudeLabelText(lat, labelFormat) +
|
|
||||||
"\n" + GetLongitudeLabelText(lon, labelFormat);
|
|
||||||
|
|
||||||
labels.Add(new Label(text, position.X, position.Y, rotation));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetLatitudeLabelText(double value, string labelFormat)
|
|
||||||
{
|
|
||||||
return GetLabelText(value, labelFormat, "NS");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetLongitudeLabelText(double value, string labelFormat)
|
|
||||||
{
|
|
||||||
return GetLabelText(Location.NormalizeLongitude(value), labelFormat, "EW");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetLabelText(double value, string labelFormat, string hemispheres)
|
|
||||||
{
|
|
||||||
var hemisphere = hemispheres[0];
|
|
||||||
|
|
||||||
if (value < -1e-8) // ~1 mm
|
|
||||||
{
|
|
||||||
value = -value;
|
|
||||||
hemisphere = hemispheres[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
var seconds = (int)Math.Round(value * 3600d);
|
|
||||||
|
|
||||||
return string.Format(CultureInfo.InvariantCulture,
|
|
||||||
labelFormat, hemisphere, seconds / 3600, seconds / 60 % 60, seconds % 60);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool DrawMeridian(PathFigureCollection figures, double lon,
|
||||||
|
double latStep, ref double minLat, ref double maxLat)
|
||||||
|
{
|
||||||
|
var points = new List<Point>();
|
||||||
|
var visible = false;
|
||||||
|
|
||||||
|
for (var lat = minLat + latStep; lat < maxLat; lat += latStep)
|
||||||
|
{
|
||||||
|
var p = ParentMap.LocationToView(lat, lon);
|
||||||
|
points.Add(p);
|
||||||
|
visible = visible || ParentMap.InsideViewBounds(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var lat = minLat; lat >= -90d; lat -= latStep)
|
||||||
|
{
|
||||||
|
var p = ParentMap.LocationToView(lat, lon);
|
||||||
|
points.Insert(0, p);
|
||||||
|
|
||||||
|
if (!ParentMap.InsideViewBounds(p)) break;
|
||||||
|
|
||||||
|
minLat = lat;
|
||||||
|
visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var lat = maxLat; lat <= 90d; lat += latStep)
|
||||||
|
{
|
||||||
|
var p = ParentMap.LocationToView(lat, lon);
|
||||||
|
points.Add(p);
|
||||||
|
|
||||||
|
if (!ParentMap.InsideViewBounds(p)) break;
|
||||||
|
|
||||||
|
maxLat = lat;
|
||||||
|
visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible && points.Count >= 2)
|
||||||
|
{
|
||||||
|
figures.Add(CreatePolylineFigure(points));
|
||||||
|
}
|
||||||
|
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddLabel(List<Label> labels, string labelFormat, double lat, double lon, Point position, double rotation)
|
||||||
|
{
|
||||||
|
if (lat > -90d && lat < 90d && ParentMap.InsideViewBounds(position))
|
||||||
|
{
|
||||||
|
rotation = ((rotation + ParentMap.ViewTransform.Rotation) % 360d + 540d) % 360d - 180d;
|
||||||
|
|
||||||
|
if (rotation < -90d)
|
||||||
|
{
|
||||||
|
rotation += 180d;
|
||||||
|
}
|
||||||
|
else if (rotation > 90d)
|
||||||
|
{
|
||||||
|
rotation -= 180d;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = GetLatitudeLabelText(lat, labelFormat) +
|
||||||
|
"\n" + GetLongitudeLabelText(lon, labelFormat);
|
||||||
|
|
||||||
|
labels.Add(new Label(text, position.X, position.Y, rotation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLatitudeLabelText(double value, string labelFormat)
|
||||||
|
{
|
||||||
|
return GetLabelText(value, labelFormat, "NS");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLongitudeLabelText(double value, string labelFormat)
|
||||||
|
{
|
||||||
|
return GetLabelText(Location.NormalizeLongitude(value), labelFormat, "EW");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLabelText(double value, string labelFormat, string hemispheres)
|
||||||
|
{
|
||||||
|
var hemisphere = hemispheres[0];
|
||||||
|
|
||||||
|
if (value < -1e-8) // ~1 mm
|
||||||
|
{
|
||||||
|
value = -value;
|
||||||
|
hemisphere = hemispheres[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var seconds = (int)Math.Round(value * 3600d);
|
||||||
|
|
||||||
|
return string.Format(CultureInfo.InvariantCulture,
|
||||||
|
labelFormat, hemisphere, seconds / 3600, seconds / 60 % 60, seconds % 60);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,90 +17,89 @@ using Brush = Avalonia.Media.IBrush;
|
||||||
using PathFigureCollection = Avalonia.Media.PathFigures;
|
using PathFigureCollection = Avalonia.Media.PathFigures;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class of map grid or graticule overlays.
|
||||||
|
/// </summary>
|
||||||
|
public abstract partial class MapGrid
|
||||||
{
|
{
|
||||||
/// <summary>
|
protected class Label(string text, double x, double y, double rotation,
|
||||||
/// Base class of map grid or graticule overlays.
|
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
|
||||||
/// </summary>
|
VerticalAlignment verticalAlignment = VerticalAlignment.Center)
|
||||||
public abstract partial class MapGrid
|
|
||||||
{
|
{
|
||||||
protected class Label(string text, double x, double y, double rotation,
|
public string Text => text;
|
||||||
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
|
public double X => x;
|
||||||
VerticalAlignment verticalAlignment = VerticalAlignment.Center)
|
public double Y => y;
|
||||||
|
public double Rotation => rotation;
|
||||||
|
public HorizontalAlignment HorizontalAlignment => horizontalAlignment;
|
||||||
|
public VerticalAlignment VerticalAlignment => verticalAlignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MinLineDistanceProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapGrid, double>(nameof(MinLineDistance), 150d);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty StrokeThicknessProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapGrid, double>(nameof(StrokeThickness), 0.5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum graticule line distance in pixels. The default value is 150.
|
||||||
|
/// </summary>
|
||||||
|
public double MinLineDistance
|
||||||
|
{
|
||||||
|
get => (double)GetValue(MinLineDistanceProperty);
|
||||||
|
set => SetValue(MinLineDistanceProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double StrokeThickness
|
||||||
|
{
|
||||||
|
get => (double)GetValue(StrokeThicknessProperty);
|
||||||
|
set => SetValue(StrokeThicknessProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Brush Foreground
|
||||||
|
{
|
||||||
|
get => (Brush)GetValue(ForegroundProperty);
|
||||||
|
set => SetValue(ForegroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FontFamily FontFamily
|
||||||
|
{
|
||||||
|
get => (FontFamily)GetValue(FontFamilyProperty);
|
||||||
|
set => SetValue(FontFamilyProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double FontSize
|
||||||
|
{
|
||||||
|
get => (double)GetValue(FontSizeProperty);
|
||||||
|
set => SetValue(FontSizeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void DrawGrid(PathFigureCollection figures, List<Label> labels);
|
||||||
|
|
||||||
|
protected static PathFigure CreateLineFigure(Point p1, Point p2)
|
||||||
|
{
|
||||||
|
var figure = new PathFigure
|
||||||
{
|
{
|
||||||
public string Text => text;
|
StartPoint = p1,
|
||||||
public double X => x;
|
IsClosed = false,
|
||||||
public double Y => y;
|
IsFilled = false
|
||||||
public double Rotation => rotation;
|
};
|
||||||
public HorizontalAlignment HorizontalAlignment => horizontalAlignment;
|
|
||||||
public VerticalAlignment VerticalAlignment => verticalAlignment;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MinLineDistanceProperty =
|
figure.Segments.Add(new LineSegment { Point = p2 });
|
||||||
DependencyPropertyHelper.Register<MapGrid, double>(nameof(MinLineDistance), 150d);
|
return figure;
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty StrokeThicknessProperty =
|
protected static PathFigure CreatePolylineFigure(IEnumerable<Point> points)
|
||||||
DependencyPropertyHelper.Register<MapGrid, double>(nameof(StrokeThickness), 0.5);
|
{
|
||||||
|
var figure = new PathFigure
|
||||||
/// <summary>
|
|
||||||
/// Minimum graticule line distance in pixels. The default value is 150.
|
|
||||||
/// </summary>
|
|
||||||
public double MinLineDistance
|
|
||||||
{
|
{
|
||||||
get => (double)GetValue(MinLineDistanceProperty);
|
StartPoint = points.First(),
|
||||||
set => SetValue(MinLineDistanceProperty, value);
|
IsClosed = false,
|
||||||
}
|
IsFilled = false
|
||||||
|
};
|
||||||
|
|
||||||
public double StrokeThickness
|
figure.Segments.Add(CreatePolyLineSegment(points.Skip(1)));
|
||||||
{
|
return figure;
|
||||||
get => (double)GetValue(StrokeThicknessProperty);
|
|
||||||
set => SetValue(StrokeThicknessProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Brush Foreground
|
|
||||||
{
|
|
||||||
get => (Brush)GetValue(ForegroundProperty);
|
|
||||||
set => SetValue(ForegroundProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FontFamily FontFamily
|
|
||||||
{
|
|
||||||
get => (FontFamily)GetValue(FontFamilyProperty);
|
|
||||||
set => SetValue(FontFamilyProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double FontSize
|
|
||||||
{
|
|
||||||
get => (double)GetValue(FontSizeProperty);
|
|
||||||
set => SetValue(FontSizeProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void DrawGrid(PathFigureCollection figures, List<Label> labels);
|
|
||||||
|
|
||||||
protected static PathFigure CreateLineFigure(Point p1, Point p2)
|
|
||||||
{
|
|
||||||
var figure = new PathFigure
|
|
||||||
{
|
|
||||||
StartPoint = p1,
|
|
||||||
IsClosed = false,
|
|
||||||
IsFilled = false
|
|
||||||
};
|
|
||||||
|
|
||||||
figure.Segments.Add(new LineSegment { Point = p2 });
|
|
||||||
return figure;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static PathFigure CreatePolylineFigure(IEnumerable<Point> points)
|
|
||||||
{
|
|
||||||
var figure = new PathFigure
|
|
||||||
{
|
|
||||||
StartPoint = points.First(),
|
|
||||||
IsClosed = false,
|
|
||||||
IsFilled = false
|
|
||||||
};
|
|
||||||
|
|
||||||
figure.Segments.Add(CreatePolyLineSegment(points.Skip(1)));
|
|
||||||
return figure;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,215 +21,214 @@ using Brush = Avalonia.Media.IBrush;
|
||||||
using ImageSource = Avalonia.Media.IImage;
|
using ImageSource = Avalonia.Media.IImage;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Displays a single map image, e.g. from a Web Map Service (WMS).
|
||||||
|
/// The image must be provided by the abstract GetImageAsync() method.
|
||||||
|
/// </summary>
|
||||||
|
public abstract partial class MapImageLayer : MapPanel, IMapLayer
|
||||||
{
|
{
|
||||||
/// <summary>
|
public static readonly DependencyProperty DescriptionProperty =
|
||||||
/// Displays a single map image, e.g. from a Web Map Service (WMS).
|
DependencyPropertyHelper.Register<MapImageLayer, string>(nameof(Description));
|
||||||
/// The image must be provided by the abstract GetImageAsync() method.
|
|
||||||
/// </summary>
|
public static readonly DependencyProperty RelativeImageSizeProperty =
|
||||||
public abstract partial class MapImageLayer : MapPanel, IMapLayer
|
DependencyPropertyHelper.Register<MapImageLayer, double>(nameof(RelativeImageSize), 1d);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty UpdateIntervalProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapImageLayer, TimeSpan>(nameof(UpdateInterval), TimeSpan.FromSeconds(0.2),
|
||||||
|
(layer, oldValue, newValue) => layer.updateTimer.Interval = newValue);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty UpdateWhileViewportChangingProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapImageLayer, bool>(nameof(UpdateWhileViewportChanging));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MapBackgroundProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapImageLayer, Brush>(nameof(MapBackground));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MapForegroundProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapImageLayer, Brush>(nameof(MapForeground));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty LoadingProgressProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapImageLayer, double>(nameof(LoadingProgress), 1d);
|
||||||
|
|
||||||
|
private readonly Progress<double> loadingProgress;
|
||||||
|
private readonly UpdateTimer updateTimer;
|
||||||
|
private bool updateInProgress;
|
||||||
|
|
||||||
|
public MapImageLayer()
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty DescriptionProperty =
|
IsHitTestVisible = false;
|
||||||
DependencyPropertyHelper.Register<MapImageLayer, string>(nameof(Description));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty RelativeImageSizeProperty =
|
loadingProgress = new Progress<double>(p => SetValue(LoadingProgressProperty, p));
|
||||||
DependencyPropertyHelper.Register<MapImageLayer, double>(nameof(RelativeImageSize), 1d);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty UpdateIntervalProperty =
|
updateTimer = new UpdateTimer { Interval = UpdateInterval };
|
||||||
DependencyPropertyHelper.Register<MapImageLayer, TimeSpan>(nameof(UpdateInterval), TimeSpan.FromSeconds(0.2),
|
updateTimer.Tick += async (_, _) => await UpdateImageAsync();
|
||||||
(layer, oldValue, newValue) => layer.updateTimer.Interval = newValue);
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty UpdateWhileViewportChangingProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MapImageLayer, bool>(nameof(UpdateWhileViewportChanging));
|
/// Description of the layer. Used to display copyright information on top of the map.
|
||||||
|
/// </summary>
|
||||||
|
public string Description
|
||||||
|
{
|
||||||
|
get => (string)GetValue(DescriptionProperty);
|
||||||
|
set => SetValue(DescriptionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty MapBackgroundProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MapImageLayer, Brush>(nameof(MapBackground));
|
/// Relative size of the map image in relation to the current view size.
|
||||||
|
/// Setting a value greater than one will let MapImageLayer request images that
|
||||||
|
/// are larger than the view, in order to support smooth panning.
|
||||||
|
/// </summary>
|
||||||
|
public double RelativeImageSize
|
||||||
|
{
|
||||||
|
get => (double)GetValue(RelativeImageSizeProperty);
|
||||||
|
set => SetValue(RelativeImageSizeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty MapForegroundProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MapImageLayer, Brush>(nameof(MapForeground));
|
/// Minimum time interval between images updates.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan UpdateInterval
|
||||||
|
{
|
||||||
|
get => (TimeSpan)GetValue(UpdateIntervalProperty);
|
||||||
|
set => SetValue(UpdateIntervalProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty LoadingProgressProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MapImageLayer, double>(nameof(LoadingProgress), 1d);
|
/// Controls if images are updated while the viewport is still changing.
|
||||||
|
/// </summary>
|
||||||
|
public bool UpdateWhileViewportChanging
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(UpdateWhileViewportChangingProperty);
|
||||||
|
set => SetValue(UpdateWhileViewportChangingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
private readonly Progress<double> loadingProgress;
|
/// <summary>
|
||||||
private readonly UpdateTimer updateTimer;
|
/// Optional background brush. Sets MapBase.Background if not null and this layer is the base map layer.
|
||||||
private bool updateInProgress;
|
/// </summary>
|
||||||
|
public Brush MapBackground
|
||||||
|
{
|
||||||
|
get => (Brush)GetValue(MapBackgroundProperty);
|
||||||
|
set => SetValue(MapBackgroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public MapImageLayer()
|
/// <summary>
|
||||||
|
/// Optional foreground brush. Sets MapBase.Foreground if not null and this layer is the base map layer.
|
||||||
|
/// </summary>
|
||||||
|
public Brush MapForeground
|
||||||
|
{
|
||||||
|
get => (Brush)GetValue(MapForegroundProperty);
|
||||||
|
set => SetValue(MapForegroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the progress of the ImageLoader as a double value between 0 and 1.
|
||||||
|
/// </summary>
|
||||||
|
public double LoadingProgress => (double)GetValue(LoadingProgressProperty);
|
||||||
|
|
||||||
|
protected override void SetParentMap(MapBase map)
|
||||||
|
{
|
||||||
|
if (map != null)
|
||||||
{
|
{
|
||||||
IsHitTestVisible = false;
|
while (Children.Count < 2)
|
||||||
|
|
||||||
loadingProgress = new Progress<double>(p => SetValue(LoadingProgressProperty, p));
|
|
||||||
|
|
||||||
updateTimer = new UpdateTimer { Interval = UpdateInterval };
|
|
||||||
updateTimer.Tick += async (_, _) => await UpdateImageAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Description of the layer. Used to display copyright information on top of the map.
|
|
||||||
/// </summary>
|
|
||||||
public string Description
|
|
||||||
{
|
|
||||||
get => (string)GetValue(DescriptionProperty);
|
|
||||||
set => SetValue(DescriptionProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Relative size of the map image in relation to the current view size.
|
|
||||||
/// Setting a value greater than one will let MapImageLayer request images that
|
|
||||||
/// are larger than the view, in order to support smooth panning.
|
|
||||||
/// </summary>
|
|
||||||
public double RelativeImageSize
|
|
||||||
{
|
|
||||||
get => (double)GetValue(RelativeImageSizeProperty);
|
|
||||||
set => SetValue(RelativeImageSizeProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Minimum time interval between images updates.
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan UpdateInterval
|
|
||||||
{
|
|
||||||
get => (TimeSpan)GetValue(UpdateIntervalProperty);
|
|
||||||
set => SetValue(UpdateIntervalProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls if images are updated while the viewport is still changing.
|
|
||||||
/// </summary>
|
|
||||||
public bool UpdateWhileViewportChanging
|
|
||||||
{
|
|
||||||
get => (bool)GetValue(UpdateWhileViewportChangingProperty);
|
|
||||||
set => SetValue(UpdateWhileViewportChangingProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional background brush. Sets MapBase.Background if not null and this layer is the base map layer.
|
|
||||||
/// </summary>
|
|
||||||
public Brush MapBackground
|
|
||||||
{
|
|
||||||
get => (Brush)GetValue(MapBackgroundProperty);
|
|
||||||
set => SetValue(MapBackgroundProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional foreground brush. Sets MapBase.Foreground if not null and this layer is the base map layer.
|
|
||||||
/// </summary>
|
|
||||||
public Brush MapForeground
|
|
||||||
{
|
|
||||||
get => (Brush)GetValue(MapForegroundProperty);
|
|
||||||
set => SetValue(MapForegroundProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the progress of the ImageLoader as a double value between 0 and 1.
|
|
||||||
/// </summary>
|
|
||||||
public double LoadingProgress => (double)GetValue(LoadingProgressProperty);
|
|
||||||
|
|
||||||
protected override void SetParentMap(MapBase map)
|
|
||||||
{
|
|
||||||
if (map != null)
|
|
||||||
{
|
{
|
||||||
while (Children.Count < 2)
|
Children.Add(new Image
|
||||||
{
|
{
|
||||||
Children.Add(new Image
|
Opacity = 0d,
|
||||||
{
|
Stretch = Stretch.Fill
|
||||||
Opacity = 0d,
|
});
|
||||||
Stretch = Stretch.Fill
|
}
|
||||||
});
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
updateTimer.Stop();
|
||||||
|
ClearImages();
|
||||||
|
Children.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.SetParentMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnViewportChanged(ViewportChangedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnViewportChanged(e);
|
||||||
|
|
||||||
|
if (e.ProjectionChanged)
|
||||||
|
{
|
||||||
|
ClearImages();
|
||||||
|
await UpdateImageAsync(); // update immediately
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
updateTimer.Run(!UpdateWhileViewportChanging);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<ImageSource> GetImageAsync(Rect mapRect, IProgress<double> progress);
|
||||||
|
|
||||||
|
protected async Task UpdateImageAsync()
|
||||||
|
{
|
||||||
|
if (!updateInProgress)
|
||||||
|
{
|
||||||
|
updateInProgress = true;
|
||||||
|
updateTimer.Stop();
|
||||||
|
|
||||||
|
ImageSource image = null;
|
||||||
|
Rect? mapRect = null;
|
||||||
|
|
||||||
|
if (ParentMap != null)
|
||||||
|
{
|
||||||
|
var width = ParentMap.ActualWidth * RelativeImageSize;
|
||||||
|
var height = ParentMap.ActualHeight * RelativeImageSize;
|
||||||
|
|
||||||
|
if (width > 0d && height > 0d)
|
||||||
|
{
|
||||||
|
var x = (ParentMap.ActualWidth - width) / 2d;
|
||||||
|
var y = (ParentMap.ActualHeight - height) / 2d;
|
||||||
|
|
||||||
|
mapRect = ParentMap.ViewTransform.ViewToMapBounds(new Rect(x, y, width, height));
|
||||||
|
image = await GetImageAsync(mapRect.Value, loadingProgress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwapImages(image, mapRect);
|
||||||
|
updateInProgress = false;
|
||||||
|
}
|
||||||
|
else // update on next timer tick
|
||||||
|
{
|
||||||
|
updateTimer.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearImages()
|
||||||
|
{
|
||||||
|
foreach (var image in Children.OfType<Image>())
|
||||||
|
{
|
||||||
|
image.ClearValue(MapRectProperty);
|
||||||
|
image.ClearValue(Image.SourceProperty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SwapImages(ImageSource image, Rect? mapRect)
|
||||||
|
{
|
||||||
|
if (Children.Count >= 2)
|
||||||
|
{
|
||||||
|
var topImage = (Image)Children[0];
|
||||||
|
|
||||||
|
Children.RemoveAt(0);
|
||||||
|
Children.Insert(1, topImage);
|
||||||
|
|
||||||
|
topImage.Source = image;
|
||||||
|
SetMapRect(topImage, mapRect);
|
||||||
|
|
||||||
|
if (MapBase.ImageFadeDuration > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
FadeOver();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
updateTimer.Stop();
|
topImage.Opacity = 1d;
|
||||||
ClearImages();
|
Children[0].Opacity = 0d;
|
||||||
Children.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
base.SetParentMap(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async void OnViewportChanged(ViewportChangedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnViewportChanged(e);
|
|
||||||
|
|
||||||
if (e.ProjectionChanged)
|
|
||||||
{
|
|
||||||
ClearImages();
|
|
||||||
await UpdateImageAsync(); // update immediately
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
updateTimer.Run(!UpdateWhileViewportChanging);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract Task<ImageSource> GetImageAsync(Rect mapRect, IProgress<double> progress);
|
|
||||||
|
|
||||||
protected async Task UpdateImageAsync()
|
|
||||||
{
|
|
||||||
if (!updateInProgress)
|
|
||||||
{
|
|
||||||
updateInProgress = true;
|
|
||||||
updateTimer.Stop();
|
|
||||||
|
|
||||||
ImageSource image = null;
|
|
||||||
Rect? mapRect = null;
|
|
||||||
|
|
||||||
if (ParentMap != null)
|
|
||||||
{
|
|
||||||
var width = ParentMap.ActualWidth * RelativeImageSize;
|
|
||||||
var height = ParentMap.ActualHeight * RelativeImageSize;
|
|
||||||
|
|
||||||
if (width > 0d && height > 0d)
|
|
||||||
{
|
|
||||||
var x = (ParentMap.ActualWidth - width) / 2d;
|
|
||||||
var y = (ParentMap.ActualHeight - height) / 2d;
|
|
||||||
|
|
||||||
mapRect = ParentMap.ViewTransform.ViewToMapBounds(new Rect(x, y, width, height));
|
|
||||||
image = await GetImageAsync(mapRect.Value, loadingProgress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SwapImages(image, mapRect);
|
|
||||||
updateInProgress = false;
|
|
||||||
}
|
|
||||||
else // update on next timer tick
|
|
||||||
{
|
|
||||||
updateTimer.Run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearImages()
|
|
||||||
{
|
|
||||||
foreach (var image in Children.OfType<Image>())
|
|
||||||
{
|
|
||||||
image.ClearValue(MapRectProperty);
|
|
||||||
image.ClearValue(Image.SourceProperty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SwapImages(ImageSource image, Rect? mapRect)
|
|
||||||
{
|
|
||||||
if (Children.Count >= 2)
|
|
||||||
{
|
|
||||||
var topImage = (Image)Children[0];
|
|
||||||
|
|
||||||
Children.RemoveAt(0);
|
|
||||||
Children.Insert(1, topImage);
|
|
||||||
|
|
||||||
topImage.Source = image;
|
|
||||||
SetMapRect(topImage, mapRect);
|
|
||||||
|
|
||||||
if (MapBase.ImageFadeDuration > TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
FadeOver();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
topImage.Opacity = 1d;
|
|
||||||
Children[0].Opacity = 0d;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,101 +15,100 @@ using Avalonia.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Container class for an item in a MapItemsControl.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapItem : ListBoxItem, IMapElement
|
||||||
{
|
{
|
||||||
|
public static readonly DependencyProperty LocationProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapItem, Location>(
|
||||||
|
nameof(Location), MapPanel.LocationProperty, (item, _, _) => item.UpdateMapTransform());
|
||||||
|
|
||||||
|
public static readonly DependencyProperty AutoCollapseProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapItem, bool>(
|
||||||
|
nameof(AutoCollapse), MapPanel.AutoCollapseProperty);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Container class for an item in a MapItemsControl.
|
/// Gets/sets MapPanel.Location.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MapItem : ListBoxItem, IMapElement
|
public Location Location
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty LocationProperty =
|
get => (Location)GetValue(LocationProperty);
|
||||||
DependencyPropertyHelper.AddOwner<MapItem, Location>(
|
set => SetValue(LocationProperty, value);
|
||||||
nameof(Location), MapPanel.LocationProperty, (item, _, _) => item.UpdateMapTransform());
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty AutoCollapseProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.AddOwner<MapItem, bool>(
|
/// Gets/sets MapPanel.AutoCollapse.
|
||||||
nameof(AutoCollapse), MapPanel.AutoCollapseProperty);
|
/// </summary>
|
||||||
|
public bool AutoCollapse
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(AutoCollapseProperty);
|
||||||
|
set => SetValue(AutoCollapseProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets/sets MapPanel.Location.
|
/// Implements IMapElement.ParentMap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Location Location
|
public MapBase ParentMap
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set
|
||||||
{
|
{
|
||||||
get => (Location)GetValue(LocationProperty);
|
if (field != null)
|
||||||
set => SetValue(LocationProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets/sets MapPanel.AutoCollapse.
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoCollapse
|
|
||||||
{
|
|
||||||
get => (bool)GetValue(AutoCollapseProperty);
|
|
||||||
set => SetValue(AutoCollapseProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implements IMapElement.ParentMap.
|
|
||||||
/// </summary>
|
|
||||||
public MapBase ParentMap
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set
|
|
||||||
{
|
{
|
||||||
if (field != null)
|
field.ViewportChanged -= OnViewportChanged;
|
||||||
{
|
}
|
||||||
field.ViewportChanged -= OnViewportChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
field = value;
|
field = value;
|
||||||
|
|
||||||
if (field != null && mapTransform != null)
|
if (field != null && mapTransform != null)
|
||||||
|
{
|
||||||
|
// Attach ViewportChanged handler only if MapTransform is actually used.
|
||||||
|
//
|
||||||
|
field.ViewportChanged += OnViewportChanged;
|
||||||
|
UpdateMapTransform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a Transform for scaling and rotating geometries
|
||||||
|
/// in map coordinates (meters) to view coordinates (pixels).
|
||||||
|
/// </summary>
|
||||||
|
public MatrixTransform MapTransform
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (mapTransform == null)
|
||||||
|
{
|
||||||
|
mapTransform = new MatrixTransform();
|
||||||
|
|
||||||
|
if (ParentMap != null)
|
||||||
{
|
{
|
||||||
// Attach ViewportChanged handler only if MapTransform is actually used.
|
ParentMap.ViewportChanged += OnViewportChanged;
|
||||||
//
|
|
||||||
field.ViewportChanged += OnViewportChanged;
|
|
||||||
UpdateMapTransform();
|
UpdateMapTransform();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
return mapTransform;
|
||||||
/// Gets a Transform for scaling and rotating geometries
|
|
||||||
/// in map coordinates (meters) to view coordinates (pixels).
|
|
||||||
/// </summary>
|
|
||||||
public MatrixTransform MapTransform
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (mapTransform == null)
|
|
||||||
{
|
|
||||||
mapTransform = new MatrixTransform();
|
|
||||||
|
|
||||||
if (ParentMap != null)
|
|
||||||
{
|
|
||||||
ParentMap.ViewportChanged += OnViewportChanged;
|
|
||||||
|
|
||||||
UpdateMapTransform();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapTransform;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MatrixTransform mapTransform;
|
|
||||||
|
|
||||||
private void UpdateMapTransform()
|
|
||||||
{
|
|
||||||
if (mapTransform != null && ParentMap != null && Location != null)
|
|
||||||
{
|
|
||||||
mapTransform.Matrix = ParentMap.GetMapToViewTransform(Location);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
|
||||||
{
|
|
||||||
UpdateMapTransform();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MatrixTransform mapTransform;
|
||||||
|
|
||||||
|
private void UpdateMapTransform()
|
||||||
|
{
|
||||||
|
if (mapTransform != null && ParentMap != null && Location != null)
|
||||||
|
{
|
||||||
|
mapTransform.Matrix = ParentMap.GetMapToViewTransform(Location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
||||||
|
{
|
||||||
|
UpdateMapTransform();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,126 +18,125 @@ using Avalonia.Data;
|
||||||
using PropertyPath = System.String;
|
using PropertyPath = System.String;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An ItemsControl with selectable items on a Map. Uses MapItem as item container.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapItemsControl : ListBox
|
||||||
{
|
{
|
||||||
|
public static readonly DependencyProperty LocationMemberPathProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapItemsControl, string>(nameof(LocationMemberPath));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An ItemsControl with selectable items on a Map. Uses MapItem as item container.
|
/// Path to a source property for binding the Location property of MapItem containers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MapItemsControl : ListBox
|
public string LocationMemberPath
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty LocationMemberPathProperty =
|
get => (string)GetValue(LocationMemberPathProperty);
|
||||||
DependencyPropertyHelper.Register<MapItemsControl, string>(nameof(LocationMemberPath));
|
set => SetValue(LocationMemberPathProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void SelectItems(Predicate<object> predicate)
|
||||||
/// Path to a source property for binding the Location property of MapItem containers.
|
{
|
||||||
/// </summary>
|
if (SelectionMode == SelectionMode.Single)
|
||||||
public string LocationMemberPath
|
|
||||||
{
|
{
|
||||||
get => (string)GetValue(LocationMemberPathProperty);
|
throw new InvalidOperationException("SelectionMode must not be Single");
|
||||||
set => SetValue(LocationMemberPathProperty, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SelectItems(Predicate<object> predicate)
|
foreach (var item in Items)
|
||||||
{
|
{
|
||||||
if (SelectionMode == SelectionMode.Single)
|
var selected = predicate(item);
|
||||||
{
|
|
||||||
throw new InvalidOperationException("SelectionMode must not be Single");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in Items)
|
if (selected != SelectedItems.Contains(item))
|
||||||
{
|
{
|
||||||
var selected = predicate(item);
|
if (selected)
|
||||||
|
|
||||||
if (selected != SelectedItems.Contains(item))
|
|
||||||
{
|
{
|
||||||
if (selected)
|
SelectedItems.Add(item);
|
||||||
{
|
|
||||||
SelectedItems.Add(item);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SelectedItems.Remove(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
}
|
|
||||||
|
|
||||||
public void SelectItemsByLocation(Predicate<Location> predicate)
|
|
||||||
{
|
|
||||||
SelectItems(item =>
|
|
||||||
{
|
|
||||||
var location = MapPanel.GetLocation(ContainerFromItem(item));
|
|
||||||
|
|
||||||
return location!= null && predicate(location);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SelectItemsByPosition(Predicate<Point> predicate)
|
|
||||||
{
|
|
||||||
SelectItems(item =>
|
|
||||||
{
|
|
||||||
var position = MapPanel.GetViewPosition(ContainerFromItem(item));
|
|
||||||
|
|
||||||
return position.HasValue && predicate(position.Value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SelectItemsInRect(Rect rect)
|
|
||||||
{
|
|
||||||
SelectItemsByPosition(rect.Contains);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Selects all items in a rectangular range between SelectedItem and the specified MapItem.
|
|
||||||
/// </summary>
|
|
||||||
internal void SelectItemsInRange(MapItem mapItem)
|
|
||||||
{
|
|
||||||
var position = MapPanel.GetViewPosition(mapItem);
|
|
||||||
|
|
||||||
if (position.HasValue)
|
|
||||||
{
|
|
||||||
var xMin = position.Value.X;
|
|
||||||
var xMax = position.Value.X;
|
|
||||||
var yMin = position.Value.Y;
|
|
||||||
var yMax = position.Value.Y;
|
|
||||||
|
|
||||||
if (SelectedItem != null)
|
|
||||||
{
|
{
|
||||||
var selectedMapItem = ContainerFromItem(SelectedItem);
|
SelectedItems.Remove(item);
|
||||||
|
|
||||||
if (selectedMapItem != mapItem)
|
|
||||||
{
|
|
||||||
position = MapPanel.GetViewPosition(selectedMapItem);
|
|
||||||
|
|
||||||
if (position.HasValue)
|
|
||||||
{
|
|
||||||
xMin = Math.Min(xMin, position.Value.X);
|
|
||||||
xMax = Math.Max(xMax, position.Value.X);
|
|
||||||
yMin = Math.Min(yMin, position.Value.Y);
|
|
||||||
yMax = Math.Max(yMax, position.Value.Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectItemsInRect(new Rect(xMin, yMin, xMax - xMin, yMax - yMin));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PrepareContainer(MapItem mapItem, object item)
|
|
||||||
{
|
|
||||||
if (LocationMemberPath != null)
|
|
||||||
{
|
|
||||||
mapItem.SetBinding(MapItem.LocationProperty,
|
|
||||||
new Binding { Source = item, Path = new PropertyPath(LocationMemberPath) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearContainer(MapItem mapItem)
|
|
||||||
{
|
|
||||||
if (LocationMemberPath != null)
|
|
||||||
{
|
|
||||||
mapItem.ClearValue(MapItem.LocationProperty);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SelectItemsByLocation(Predicate<Location> predicate)
|
||||||
|
{
|
||||||
|
SelectItems(item =>
|
||||||
|
{
|
||||||
|
var location = MapPanel.GetLocation(ContainerFromItem(item));
|
||||||
|
|
||||||
|
return location!= null && predicate(location);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SelectItemsByPosition(Predicate<Point> predicate)
|
||||||
|
{
|
||||||
|
SelectItems(item =>
|
||||||
|
{
|
||||||
|
var position = MapPanel.GetViewPosition(ContainerFromItem(item));
|
||||||
|
|
||||||
|
return position.HasValue && predicate(position.Value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SelectItemsInRect(Rect rect)
|
||||||
|
{
|
||||||
|
SelectItemsByPosition(rect.Contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selects all items in a rectangular range between SelectedItem and the specified MapItem.
|
||||||
|
/// </summary>
|
||||||
|
internal void SelectItemsInRange(MapItem mapItem)
|
||||||
|
{
|
||||||
|
var position = MapPanel.GetViewPosition(mapItem);
|
||||||
|
|
||||||
|
if (position.HasValue)
|
||||||
|
{
|
||||||
|
var xMin = position.Value.X;
|
||||||
|
var xMax = position.Value.X;
|
||||||
|
var yMin = position.Value.Y;
|
||||||
|
var yMax = position.Value.Y;
|
||||||
|
|
||||||
|
if (SelectedItem != null)
|
||||||
|
{
|
||||||
|
var selectedMapItem = ContainerFromItem(SelectedItem);
|
||||||
|
|
||||||
|
if (selectedMapItem != mapItem)
|
||||||
|
{
|
||||||
|
position = MapPanel.GetViewPosition(selectedMapItem);
|
||||||
|
|
||||||
|
if (position.HasValue)
|
||||||
|
{
|
||||||
|
xMin = Math.Min(xMin, position.Value.X);
|
||||||
|
xMax = Math.Max(xMax, position.Value.X);
|
||||||
|
yMin = Math.Min(yMin, position.Value.Y);
|
||||||
|
yMax = Math.Max(yMax, position.Value.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectItemsInRect(new Rect(xMin, yMin, xMax - xMin, yMax - yMin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrepareContainer(MapItem mapItem, object item)
|
||||||
|
{
|
||||||
|
if (LocationMemberPath != null)
|
||||||
|
{
|
||||||
|
mapItem.SetBinding(MapItem.LocationProperty,
|
||||||
|
new Binding { Source = item, Path = new PropertyPath(LocationMemberPath) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearContainer(MapItem mapItem)
|
||||||
|
{
|
||||||
|
if (LocationMemberPath != null)
|
||||||
|
{
|
||||||
|
mapItem.ClearValue(MapItem.LocationProperty);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,34 +7,33 @@ using Windows.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A multi-polygon defined by a collection of collections of Locations.
|
||||||
|
/// Allows to draw filled polygons with holes.
|
||||||
|
///
|
||||||
|
/// A PolygonCollection (with ObservableCollection of Location elements) may be used
|
||||||
|
/// for the Polygons property if collection changes of the property itself and its
|
||||||
|
/// elements are both supposed to trigger UI updates.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapMultiPolygon : MapPolypoint
|
||||||
{
|
{
|
||||||
|
public static readonly DependencyProperty PolygonsProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapMultiPolygon, IEnumerable<IEnumerable<Location>>>(nameof(Polygons), null,
|
||||||
|
(polygon, oldValue, newValue) => polygon.DataCollectionPropertyChanged(oldValue, newValue));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A multi-polygon defined by a collection of collections of Locations.
|
/// Gets or sets the Locations that define the multi-polygon points.
|
||||||
/// Allows to draw filled polygons with holes.
|
|
||||||
///
|
|
||||||
/// A PolygonCollection (with ObservableCollection of Location elements) may be used
|
|
||||||
/// for the Polygons property if collection changes of the property itself and its
|
|
||||||
/// elements are both supposed to trigger UI updates.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MapMultiPolygon : MapPolypoint
|
public IEnumerable<IEnumerable<Location>> Polygons
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty PolygonsProperty =
|
get => (IEnumerable<IEnumerable<Location>>)GetValue(PolygonsProperty);
|
||||||
DependencyPropertyHelper.Register<MapMultiPolygon, IEnumerable<IEnumerable<Location>>>(nameof(Polygons), null,
|
set => SetValue(PolygonsProperty, value);
|
||||||
(polygon, oldValue, newValue) => polygon.DataCollectionPropertyChanged(oldValue, newValue));
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override void UpdateData()
|
||||||
/// Gets or sets the Locations that define the multi-polygon points.
|
{
|
||||||
/// </summary>
|
UpdateData(Polygons);
|
||||||
public IEnumerable<IEnumerable<Location>> Polygons
|
|
||||||
{
|
|
||||||
get => (IEnumerable<IEnumerable<Location>>)GetValue(PolygonsProperty);
|
|
||||||
set => SetValue(PolygonsProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateData()
|
|
||||||
{
|
|
||||||
UpdateData(Polygons);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,110 +11,109 @@ using Windows.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A MapPanel with a collection of GroundOverlay or GeoImage children.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapOverlaysPanel : MapPanel
|
||||||
{
|
{
|
||||||
/// <summary>
|
public static readonly DependencyProperty SourcePathsProperty =
|
||||||
/// A MapPanel with a collection of GroundOverlay or GeoImage children.
|
DependencyPropertyHelper.Register<MapOverlaysPanel, IEnumerable<string>>(nameof(SourcePaths), null,
|
||||||
/// </summary>
|
async (control, oldValue, newValue) => await control.SourcePathsPropertyChanged(oldValue, newValue));
|
||||||
public partial class MapOverlaysPanel : MapPanel
|
|
||||||
|
public IEnumerable<string> SourcePaths
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty SourcePathsProperty =
|
get => (IEnumerable<string>)GetValue(SourcePathsProperty);
|
||||||
DependencyPropertyHelper.Register<MapOverlaysPanel, IEnumerable<string>>(nameof(SourcePaths), null,
|
set => SetValue(SourcePathsProperty, value);
|
||||||
async (control, oldValue, newValue) => await control.SourcePathsPropertyChanged(oldValue, newValue));
|
}
|
||||||
|
|
||||||
public IEnumerable<string> SourcePaths
|
private async Task SourcePathsPropertyChanged(IEnumerable<string> oldSourcePaths, IEnumerable<string> newSourcePaths)
|
||||||
|
{
|
||||||
|
Children.Clear();
|
||||||
|
|
||||||
|
if (oldSourcePaths is INotifyCollectionChanged oldCollection)
|
||||||
{
|
{
|
||||||
get => (IEnumerable<string>)GetValue(SourcePathsProperty);
|
oldCollection.CollectionChanged -= SourcePathsCollectionChanged;
|
||||||
set => SetValue(SourcePathsProperty, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SourcePathsPropertyChanged(IEnumerable<string> oldSourcePaths, IEnumerable<string> newSourcePaths)
|
if (newSourcePaths != null)
|
||||||
{
|
{
|
||||||
Children.Clear();
|
if (newSourcePaths is INotifyCollectionChanged newCollection)
|
||||||
|
|
||||||
if (oldSourcePaths is INotifyCollectionChanged oldCollection)
|
|
||||||
{
|
{
|
||||||
oldCollection.CollectionChanged -= SourcePathsCollectionChanged;
|
newCollection.CollectionChanged += SourcePathsCollectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newSourcePaths != null)
|
await AddOverlays(0, newSourcePaths);
|
||||||
{
|
|
||||||
if (newSourcePaths is INotifyCollectionChanged newCollection)
|
|
||||||
{
|
|
||||||
newCollection.CollectionChanged += SourcePathsCollectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
await AddOverlays(0, newSourcePaths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void SourcePathsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
switch (e.Action)
|
|
||||||
{
|
|
||||||
case NotifyCollectionChangedAction.Add:
|
|
||||||
await AddOverlays(e.NewStartingIndex, e.NewItems.Cast<string>());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Remove:
|
|
||||||
RemoveOverlays(e.OldStartingIndex, e.OldItems.Count);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Move:
|
|
||||||
RemoveOverlays(e.OldStartingIndex, e.OldItems.Count);
|
|
||||||
await AddOverlays(e.NewStartingIndex, e.NewItems.Cast<string>());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Replace:
|
|
||||||
await ReplaceOverlays(e.NewStartingIndex, e.NewItems.Cast<string>());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Reset:
|
|
||||||
Children.Clear();
|
|
||||||
await AddOverlays(0, SourcePaths);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddOverlays(int index, IEnumerable<string> sourcePaths)
|
|
||||||
{
|
|
||||||
foreach (var sourcePath in sourcePaths)
|
|
||||||
{
|
|
||||||
Children.Insert(index++, await CreateOverlayAsync(sourcePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReplaceOverlays(int index, IEnumerable<string> sourcePaths)
|
|
||||||
{
|
|
||||||
foreach (var sourcePath in sourcePaths)
|
|
||||||
{
|
|
||||||
Children[index++] = await CreateOverlayAsync(sourcePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveOverlays(int index, int count)
|
|
||||||
{
|
|
||||||
while (--count >= 0)
|
|
||||||
{
|
|
||||||
Children.RemoveAt(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual async Task<FrameworkElement> CreateOverlayAsync(string sourcePath)
|
|
||||||
{
|
|
||||||
FrameworkElement overlay;
|
|
||||||
var ext = Path.GetExtension(sourcePath).ToLower();
|
|
||||||
|
|
||||||
if (ext == ".kmz" || ext == ".kml")
|
|
||||||
{
|
|
||||||
overlay = await GroundOverlay.CreateAsync(sourcePath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
overlay = await GeoImage.CreateAsync(sourcePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return overlay;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void SourcePathsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
await AddOverlays(e.NewStartingIndex, e.NewItems.Cast<string>());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
RemoveOverlays(e.OldStartingIndex, e.OldItems.Count);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyCollectionChangedAction.Move:
|
||||||
|
RemoveOverlays(e.OldStartingIndex, e.OldItems.Count);
|
||||||
|
await AddOverlays(e.NewStartingIndex, e.NewItems.Cast<string>());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyCollectionChangedAction.Replace:
|
||||||
|
await ReplaceOverlays(e.NewStartingIndex, e.NewItems.Cast<string>());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyCollectionChangedAction.Reset:
|
||||||
|
Children.Clear();
|
||||||
|
await AddOverlays(0, SourcePaths);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddOverlays(int index, IEnumerable<string> sourcePaths)
|
||||||
|
{
|
||||||
|
foreach (var sourcePath in sourcePaths)
|
||||||
|
{
|
||||||
|
Children.Insert(index++, await CreateOverlayAsync(sourcePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReplaceOverlays(int index, IEnumerable<string> sourcePaths)
|
||||||
|
{
|
||||||
|
foreach (var sourcePath in sourcePaths)
|
||||||
|
{
|
||||||
|
Children[index++] = await CreateOverlayAsync(sourcePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveOverlays(int index, int count)
|
||||||
|
{
|
||||||
|
while (--count >= 0)
|
||||||
|
{
|
||||||
|
Children.RemoveAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual async Task<FrameworkElement> CreateOverlayAsync(string sourcePath)
|
||||||
|
{
|
||||||
|
FrameworkElement overlay;
|
||||||
|
var ext = Path.GetExtension(sourcePath).ToLower();
|
||||||
|
|
||||||
|
if (ext == ".kmz" || ext == ".kml")
|
||||||
|
{
|
||||||
|
overlay = await GroundOverlay.CreateAsync(sourcePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
overlay = await GeoImage.CreateAsync(sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,402 +24,401 @@ using Avalonia.Media;
|
||||||
/// Arranges child elements on a Map at positions specified by the attached property Location,
|
/// Arranges child elements on a Map at positions specified by the attached property Location,
|
||||||
/// or in rectangles specified by the attached property BoundingBox.
|
/// or in rectangles specified by the attached property BoundingBox.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional interface to hold the value of the attached property MapPanel.ParentMap.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMapElement
|
||||||
{
|
{
|
||||||
/// <summary>
|
MapBase ParentMap { get; set; }
|
||||||
/// Optional interface to hold the value of the attached property MapPanel.ParentMap.
|
}
|
||||||
/// </summary>
|
|
||||||
public interface IMapElement
|
public partial class MapPanel : Panel, IMapElement
|
||||||
|
{
|
||||||
|
private static readonly DependencyProperty ViewPositionProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<Point?>("ViewPosition", typeof(MapPanel));
|
||||||
|
|
||||||
|
private static readonly DependencyProperty ParentMapProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<MapBase>("ParentMap", typeof(MapPanel), null,
|
||||||
|
(element, oldValue, newValue) =>
|
||||||
|
{
|
||||||
|
if (element is IMapElement mapElement)
|
||||||
|
{
|
||||||
|
mapElement.ParentMap = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if WPF || AVALONIA
|
||||||
|
, true // inherits, not available in WinUI/UWP
|
||||||
|
#endif
|
||||||
|
);
|
||||||
|
|
||||||
|
public MapPanel()
|
||||||
{
|
{
|
||||||
MapBase ParentMap { get; set; }
|
if (this is MapBase)
|
||||||
|
{
|
||||||
|
FlowDirection = FlowDirection.LeftToRight;
|
||||||
|
SetValue(ParentMapProperty, this);
|
||||||
|
}
|
||||||
|
#if UWP || WINUI
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InitMapElement(this);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class MapPanel : Panel, IMapElement
|
private MapBase parentMap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements IMapElement.ParentMap.
|
||||||
|
/// </summary>
|
||||||
|
public MapBase ParentMap
|
||||||
{
|
{
|
||||||
private static readonly DependencyProperty ViewPositionProperty =
|
get => parentMap;
|
||||||
DependencyPropertyHelper.RegisterAttached<Point?>("ViewPosition", typeof(MapPanel));
|
set => SetParentMap(value);
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly DependencyProperty ParentMapProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.RegisterAttached<MapBase>("ParentMap", typeof(MapPanel), null,
|
/// Gets a value that controls whether an element's Visibility is automatically
|
||||||
(element, oldValue, newValue) =>
|
/// set to Collapsed when it is located outside the visible viewport area.
|
||||||
{
|
/// </summary>
|
||||||
if (element is IMapElement mapElement)
|
public static bool GetAutoCollapse(FrameworkElement element)
|
||||||
{
|
{
|
||||||
mapElement.ParentMap = newValue;
|
return (bool)element.GetValue(AutoCollapseProperty);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#if WPF || AVALONIA
|
|
||||||
, true // inherits, not available in WinUI/UWP
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
|
|
||||||
public MapPanel()
|
/// <summary>
|
||||||
|
/// Sets the AutoCollapse property.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetAutoCollapse(FrameworkElement element, bool value)
|
||||||
|
{
|
||||||
|
element.SetValue(AutoCollapseProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Location of an element.
|
||||||
|
/// </summary>
|
||||||
|
public static Location GetLocation(FrameworkElement element)
|
||||||
|
{
|
||||||
|
return (Location)element.GetValue(LocationProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the Location of an element.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetLocation(FrameworkElement element, Location value)
|
||||||
|
{
|
||||||
|
element.SetValue(LocationProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the BoundingBox of an element.
|
||||||
|
/// </summary>
|
||||||
|
public static BoundingBox GetBoundingBox(FrameworkElement element)
|
||||||
|
{
|
||||||
|
return (BoundingBox)element.GetValue(BoundingBoxProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the BoundingBox of an element.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetBoundingBox(FrameworkElement element, BoundingBox value)
|
||||||
|
{
|
||||||
|
element.SetValue(BoundingBoxProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the MapRect of an element.
|
||||||
|
/// </summary>
|
||||||
|
public static Rect? GetMapRect(FrameworkElement element)
|
||||||
|
{
|
||||||
|
return (Rect?)element.GetValue(MapRectProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the MapRect of an element.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetMapRect(FrameworkElement element, Rect? value)
|
||||||
|
{
|
||||||
|
element.SetValue(MapRectProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the view position of an element with Location.
|
||||||
|
/// </summary>
|
||||||
|
public static Point? GetViewPosition(FrameworkElement element)
|
||||||
|
{
|
||||||
|
return (Point?)element.GetValue(ViewPositionProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the attached ViewPosition property of an element. The method is called during
|
||||||
|
/// ArrangeOverride and may be overridden to modify the actual view position value.
|
||||||
|
/// An overridden method should call this method to set the attached property.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual Point SetViewPosition(FrameworkElement element, Point position)
|
||||||
|
{
|
||||||
|
element.SetValue(ViewPositionProperty, position);
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void SetParentMap(MapBase map)
|
||||||
|
{
|
||||||
|
if (parentMap != null && parentMap != this)
|
||||||
{
|
{
|
||||||
if (this is MapBase)
|
parentMap.ViewportChanged -= OnViewportChanged;
|
||||||
{
|
|
||||||
FlowDirection = FlowDirection.LeftToRight;
|
|
||||||
SetValue(ParentMapProperty, this);
|
|
||||||
}
|
|
||||||
#if UWP || WINUI
|
|
||||||
else
|
|
||||||
{
|
|
||||||
InitMapElement(this);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MapBase parentMap;
|
parentMap = map;
|
||||||
|
|
||||||
/// <summary>
|
if (parentMap != null && parentMap != this)
|
||||||
/// Implements IMapElement.ParentMap.
|
|
||||||
/// </summary>
|
|
||||||
public MapBase ParentMap
|
|
||||||
{
|
{
|
||||||
get => parentMap;
|
parentMap.ViewportChanged += OnViewportChanged;
|
||||||
set => SetParentMap(value);
|
|
||||||
|
OnViewportChanged(new ViewportChangedEventArgs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
||||||
|
{
|
||||||
|
OnViewportChanged(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
|
||||||
|
{
|
||||||
|
InvalidateArrange();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
|
{
|
||||||
|
availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
|
||||||
|
|
||||||
|
foreach (var element in Children.Cast<FrameworkElement>())
|
||||||
|
{
|
||||||
|
element.Measure(availableSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return new Size();
|
||||||
/// Gets a value that controls whether an element's Visibility is automatically
|
}
|
||||||
/// set to Collapsed when it is located outside the visible viewport area.
|
|
||||||
/// </summary>
|
protected override Size ArrangeOverride(Size finalSize)
|
||||||
public static bool GetAutoCollapse(FrameworkElement element)
|
{
|
||||||
|
if (parentMap != null)
|
||||||
{
|
{
|
||||||
return (bool)element.GetValue(AutoCollapseProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the AutoCollapse property.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetAutoCollapse(FrameworkElement element, bool value)
|
|
||||||
{
|
|
||||||
element.SetValue(AutoCollapseProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the Location of an element.
|
|
||||||
/// </summary>
|
|
||||||
public static Location GetLocation(FrameworkElement element)
|
|
||||||
{
|
|
||||||
return (Location)element.GetValue(LocationProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the Location of an element.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetLocation(FrameworkElement element, Location value)
|
|
||||||
{
|
|
||||||
element.SetValue(LocationProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the BoundingBox of an element.
|
|
||||||
/// </summary>
|
|
||||||
public static BoundingBox GetBoundingBox(FrameworkElement element)
|
|
||||||
{
|
|
||||||
return (BoundingBox)element.GetValue(BoundingBoxProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the BoundingBox of an element.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetBoundingBox(FrameworkElement element, BoundingBox value)
|
|
||||||
{
|
|
||||||
element.SetValue(BoundingBoxProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the MapRect of an element.
|
|
||||||
/// </summary>
|
|
||||||
public static Rect? GetMapRect(FrameworkElement element)
|
|
||||||
{
|
|
||||||
return (Rect?)element.GetValue(MapRectProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the MapRect of an element.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetMapRect(FrameworkElement element, Rect? value)
|
|
||||||
{
|
|
||||||
element.SetValue(MapRectProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the view position of an element with Location.
|
|
||||||
/// </summary>
|
|
||||||
public static Point? GetViewPosition(FrameworkElement element)
|
|
||||||
{
|
|
||||||
return (Point?)element.GetValue(ViewPositionProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the attached ViewPosition property of an element. The method is called during
|
|
||||||
/// ArrangeOverride and may be overridden to modify the actual view position value.
|
|
||||||
/// An overridden method should call this method to set the attached property.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual Point SetViewPosition(FrameworkElement element, Point position)
|
|
||||||
{
|
|
||||||
element.SetValue(ViewPositionProperty, position);
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void SetParentMap(MapBase map)
|
|
||||||
{
|
|
||||||
if (parentMap != null && parentMap != this)
|
|
||||||
{
|
|
||||||
parentMap.ViewportChanged -= OnViewportChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
parentMap = map;
|
|
||||||
|
|
||||||
if (parentMap != null && parentMap != this)
|
|
||||||
{
|
|
||||||
parentMap.ViewportChanged += OnViewportChanged;
|
|
||||||
|
|
||||||
OnViewportChanged(new ViewportChangedEventArgs());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
|
||||||
{
|
|
||||||
OnViewportChanged(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
|
|
||||||
{
|
|
||||||
InvalidateArrange();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Size MeasureOverride(Size availableSize)
|
|
||||||
{
|
|
||||||
availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
|
|
||||||
|
|
||||||
foreach (var element in Children.Cast<FrameworkElement>())
|
foreach (var element in Children.Cast<FrameworkElement>())
|
||||||
{
|
{
|
||||||
element.Measure(availableSize);
|
ArrangeChildElement(element, finalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Size();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Size ArrangeOverride(Size finalSize)
|
return finalSize;
|
||||||
{
|
}
|
||||||
if (parentMap != null)
|
|
||||||
{
|
|
||||||
foreach (var element in Children.Cast<FrameworkElement>())
|
|
||||||
{
|
|
||||||
ArrangeChildElement(element, finalSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalSize;
|
private Point GetViewPosition(Location location)
|
||||||
|
{
|
||||||
|
var position = parentMap.LocationToView(location);
|
||||||
|
|
||||||
|
if (parentMap.MapProjection.IsNormalCylindrical && !parentMap.InsideViewBounds(position))
|
||||||
|
{
|
||||||
|
var longitude = parentMap.NearestLongitude(location.Longitude);
|
||||||
|
|
||||||
|
if (!location.LongitudeEquals(longitude))
|
||||||
|
{
|
||||||
|
position = parentMap.LocationToView(location.Latitude, longitude);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Point GetViewPosition(Location location)
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Rect GetViewRect(Rect mapRect)
|
||||||
|
{
|
||||||
|
var center = new Point(mapRect.X + mapRect.Width / 2d, mapRect.Y + mapRect.Height / 2d);
|
||||||
|
var position = parentMap.ViewTransform.MapToView(center);
|
||||||
|
|
||||||
|
if (parentMap.MapProjection.IsNormalCylindrical && !parentMap.InsideViewBounds(position))
|
||||||
{
|
{
|
||||||
var position = parentMap.LocationToView(location);
|
var location = parentMap.MapProjection.MapToLocation(center);
|
||||||
|
var longitude = parentMap.NearestLongitude(location.Longitude);
|
||||||
|
|
||||||
if (parentMap.MapProjection.IsNormalCylindrical && !parentMap.InsideViewBounds(position))
|
if (!location.LongitudeEquals(longitude))
|
||||||
{
|
{
|
||||||
var longitude = parentMap.NearestLongitude(location.Longitude);
|
position = parentMap.LocationToView(location.Latitude, longitude);
|
||||||
|
|
||||||
if (!location.LongitudeEquals(longitude))
|
|
||||||
{
|
|
||||||
position = parentMap.LocationToView(location.Latitude, longitude);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Rect GetViewRect(Rect mapRect)
|
var width = mapRect.Width * parentMap.ViewTransform.Scale;
|
||||||
|
var height = mapRect.Height * parentMap.ViewTransform.Scale;
|
||||||
|
var x = position.X - width / 2d;
|
||||||
|
var y = position.Y - height / 2d;
|
||||||
|
|
||||||
|
return new Rect(x, y, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ArrangeChildElement(FrameworkElement element, Size panelSize)
|
||||||
|
{
|
||||||
|
var location = GetLocation(element);
|
||||||
|
|
||||||
|
if (location != null)
|
||||||
{
|
{
|
||||||
var center = new Point(mapRect.X + mapRect.Width / 2d, mapRect.Y + mapRect.Height / 2d);
|
var position = SetViewPosition(element, GetViewPosition(location));
|
||||||
var position = parentMap.ViewTransform.MapToView(center);
|
|
||||||
|
|
||||||
if (parentMap.MapProjection.IsNormalCylindrical && !parentMap.InsideViewBounds(position))
|
if (GetAutoCollapse(element))
|
||||||
{
|
{
|
||||||
var location = parentMap.MapProjection.MapToLocation(center);
|
element.SetVisible(parentMap.InsideViewBounds(position));
|
||||||
var longitude = parentMap.NearestLongitude(location.Longitude);
|
|
||||||
|
|
||||||
if (!location.LongitudeEquals(longitude))
|
|
||||||
{
|
|
||||||
position = parentMap.LocationToView(location.Latitude, longitude);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var width = mapRect.Width * parentMap.ViewTransform.Scale;
|
ArrangeElement(element, position);
|
||||||
var height = mapRect.Height * parentMap.ViewTransform.Scale;
|
|
||||||
var x = position.X - width / 2d;
|
|
||||||
var y = position.Y - height / 2d;
|
|
||||||
|
|
||||||
return new Rect(x, y, width, height);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
private void ArrangeChildElement(FrameworkElement element, Size panelSize)
|
|
||||||
{
|
{
|
||||||
var location = GetLocation(element);
|
element.ClearValue(ViewPositionProperty);
|
||||||
|
|
||||||
if (location != null)
|
var mapRect = GetMapRect(element);
|
||||||
|
|
||||||
|
if (mapRect.HasValue)
|
||||||
{
|
{
|
||||||
var position = SetViewPosition(element, GetViewPosition(location));
|
ArrangeElement(element, mapRect.Value, 0d);
|
||||||
|
|
||||||
if (GetAutoCollapse(element))
|
|
||||||
{
|
|
||||||
element.SetVisible(parentMap.InsideViewBounds(position));
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrangeElement(element, position);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
element.ClearValue(ViewPositionProperty);
|
var boundingBox = GetBoundingBox(element);
|
||||||
|
|
||||||
var mapRect = GetMapRect(element);
|
if (boundingBox != null)
|
||||||
|
|
||||||
if (mapRect.HasValue)
|
|
||||||
{
|
{
|
||||||
ArrangeElement(element, mapRect.Value, 0d);
|
(var rect, var rotation) = parentMap.MapProjection.BoundingBoxToMap(boundingBox);
|
||||||
|
|
||||||
|
ArrangeElement(element, rect, -rotation);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var boundingBox = GetBoundingBox(element);
|
ArrangeElement(element, panelSize);
|
||||||
|
|
||||||
if (boundingBox != null)
|
|
||||||
{
|
|
||||||
(var rect, var rotation) = parentMap.MapProjection.BoundingBoxToMap(boundingBox);
|
|
||||||
|
|
||||||
ArrangeElement(element, rect, -rotation);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ArrangeElement(element, panelSize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ArrangeElement(FrameworkElement element, Rect mapRect, double rotation)
|
private void ArrangeElement(FrameworkElement element, Rect mapRect, double rotation)
|
||||||
|
{
|
||||||
|
var viewRect = GetViewRect(mapRect);
|
||||||
|
|
||||||
|
element.Width = viewRect.Width;
|
||||||
|
element.Height = viewRect.Height;
|
||||||
|
element.Arrange(viewRect);
|
||||||
|
|
||||||
|
rotation += parentMap.ViewTransform.Rotation;
|
||||||
|
|
||||||
|
if (element.RenderTransform is RotateTransform rotateTransform)
|
||||||
{
|
{
|
||||||
var viewRect = GetViewRect(mapRect);
|
rotateTransform.Angle = rotation;
|
||||||
|
|
||||||
element.Width = viewRect.Width;
|
|
||||||
element.Height = viewRect.Height;
|
|
||||||
element.Arrange(viewRect);
|
|
||||||
|
|
||||||
rotation += parentMap.ViewTransform.Rotation;
|
|
||||||
|
|
||||||
if (element.RenderTransform is RotateTransform rotateTransform)
|
|
||||||
{
|
|
||||||
rotateTransform.Angle = rotation;
|
|
||||||
}
|
|
||||||
else if (rotation != 0d)
|
|
||||||
{
|
|
||||||
element.SetRenderTransform(new RotateTransform { Angle = rotation }, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else if (rotation != 0d)
|
||||||
private static void ArrangeElement(FrameworkElement element, Point position)
|
|
||||||
{
|
{
|
||||||
var size = GetDesiredSize(element);
|
element.SetRenderTransform(new RotateTransform { Angle = rotation }, true);
|
||||||
var x = position.X;
|
|
||||||
var y = position.Y;
|
|
||||||
|
|
||||||
switch (element.HorizontalAlignment)
|
|
||||||
{
|
|
||||||
case HorizontalAlignment.Center:
|
|
||||||
x -= size.Width / 2d;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HorizontalAlignment.Right:
|
|
||||||
x -= size.Width;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (element.VerticalAlignment)
|
|
||||||
{
|
|
||||||
case VerticalAlignment.Center:
|
|
||||||
y -= size.Height / 2d;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case VerticalAlignment.Bottom:
|
|
||||||
y -= size.Height;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
element.Arrange(new Rect(x, y, size.Width, size.Height));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ArrangeElement(FrameworkElement element, Size panelSize)
|
|
||||||
{
|
|
||||||
var size = GetDesiredSize(element);
|
|
||||||
var x = 0d;
|
|
||||||
var y = 0d;
|
|
||||||
var width = size.Width;
|
|
||||||
var height = size.Height;
|
|
||||||
|
|
||||||
switch (element.HorizontalAlignment)
|
|
||||||
{
|
|
||||||
case HorizontalAlignment.Center:
|
|
||||||
x = (panelSize.Width - size.Width) / 2d;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HorizontalAlignment.Right:
|
|
||||||
x = panelSize.Width - size.Width;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HorizontalAlignment.Stretch:
|
|
||||||
width = panelSize.Width;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (element.VerticalAlignment)
|
|
||||||
{
|
|
||||||
case VerticalAlignment.Center:
|
|
||||||
y = (panelSize.Height - size.Height) / 2d;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case VerticalAlignment.Bottom:
|
|
||||||
y = panelSize.Height - size.Height;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case VerticalAlignment.Stretch:
|
|
||||||
height = panelSize.Height;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
element.Arrange(new Rect(x, y, width, height));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Size GetDesiredSize(FrameworkElement element)
|
|
||||||
{
|
|
||||||
var width = element.DesiredSize.Width;
|
|
||||||
var height = element.DesiredSize.Height;
|
|
||||||
|
|
||||||
if (width < 0d || width == double.PositiveInfinity)
|
|
||||||
{
|
|
||||||
width = 0d;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (height < 0d || height == double.PositiveInfinity)
|
|
||||||
{
|
|
||||||
height = 0d;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Size(width, height);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ArrangeElement(FrameworkElement element, Point position)
|
||||||
|
{
|
||||||
|
var size = GetDesiredSize(element);
|
||||||
|
var x = position.X;
|
||||||
|
var y = position.Y;
|
||||||
|
|
||||||
|
switch (element.HorizontalAlignment)
|
||||||
|
{
|
||||||
|
case HorizontalAlignment.Center:
|
||||||
|
x -= size.Width / 2d;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HorizontalAlignment.Right:
|
||||||
|
x -= size.Width;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (element.VerticalAlignment)
|
||||||
|
{
|
||||||
|
case VerticalAlignment.Center:
|
||||||
|
y -= size.Height / 2d;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VerticalAlignment.Bottom:
|
||||||
|
y -= size.Height;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.Arrange(new Rect(x, y, size.Width, size.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ArrangeElement(FrameworkElement element, Size panelSize)
|
||||||
|
{
|
||||||
|
var size = GetDesiredSize(element);
|
||||||
|
var x = 0d;
|
||||||
|
var y = 0d;
|
||||||
|
var width = size.Width;
|
||||||
|
var height = size.Height;
|
||||||
|
|
||||||
|
switch (element.HorizontalAlignment)
|
||||||
|
{
|
||||||
|
case HorizontalAlignment.Center:
|
||||||
|
x = (panelSize.Width - size.Width) / 2d;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HorizontalAlignment.Right:
|
||||||
|
x = panelSize.Width - size.Width;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HorizontalAlignment.Stretch:
|
||||||
|
width = panelSize.Width;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (element.VerticalAlignment)
|
||||||
|
{
|
||||||
|
case VerticalAlignment.Center:
|
||||||
|
y = (panelSize.Height - size.Height) / 2d;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VerticalAlignment.Bottom:
|
||||||
|
y = panelSize.Height - size.Height;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VerticalAlignment.Stretch:
|
||||||
|
height = panelSize.Height;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.Arrange(new Rect(x, y, width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Size GetDesiredSize(FrameworkElement element)
|
||||||
|
{
|
||||||
|
var width = element.DesiredSize.Width;
|
||||||
|
var height = element.DesiredSize.Height;
|
||||||
|
|
||||||
|
if (width < 0d || width == double.PositiveInfinity)
|
||||||
|
{
|
||||||
|
width = 0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height < 0d || height == double.PositiveInfinity)
|
||||||
|
{
|
||||||
|
height = 0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Size(width, height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,105 +12,104 @@ using Avalonia;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A path element with a Data property that holds a Geometry in view coordinates or
|
||||||
|
/// projected map coordinates that are relative to an origin Location.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapPath : IMapElement
|
||||||
{
|
{
|
||||||
|
public static readonly DependencyProperty LocationProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapPath, Location>(nameof(Location), null,
|
||||||
|
(path, oldValue, newValue) => path.UpdateData());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A path element with a Data property that holds a Geometry in view coordinates or
|
/// Gets or sets a Location that is either used as
|
||||||
/// projected map coordinates that are relative to an origin Location.
|
/// - the origin point of a geometry specified in projected map coordinates (meters) or
|
||||||
|
/// - as an optional anchor point to constrain the view position of MapPaths with
|
||||||
|
/// multiple Locations (like MapPolyline or MapPolygon) to the visible map viewport,
|
||||||
|
/// as done for elements where the MapPanel.Location property is set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MapPath : IMapElement
|
public Location Location
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty LocationProperty =
|
get => (Location)GetValue(LocationProperty);
|
||||||
DependencyPropertyHelper.Register<MapPath, Location>(nameof(Location), null,
|
set => SetValue(LocationProperty, value);
|
||||||
(path, oldValue, newValue) => path.UpdateData());
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a Location that is either used as
|
/// Implements IMapElement.ParentMap.
|
||||||
/// - the origin point of a geometry specified in projected map coordinates (meters) or
|
/// </summary>
|
||||||
/// - as an optional anchor point to constrain the view position of MapPaths with
|
public MapBase ParentMap
|
||||||
/// multiple Locations (like MapPolyline or MapPolygon) to the visible map viewport,
|
{
|
||||||
/// as done for elements where the MapPanel.Location property is set.
|
get;
|
||||||
/// </summary>
|
set
|
||||||
public Location Location
|
|
||||||
{
|
{
|
||||||
get => (Location)GetValue(LocationProperty);
|
if (field != null)
|
||||||
set => SetValue(LocationProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implements IMapElement.ParentMap.
|
|
||||||
/// </summary>
|
|
||||||
public MapBase ParentMap
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set
|
|
||||||
{
|
{
|
||||||
if (field != null)
|
field.ViewportChanged -= OnViewportChanged;
|
||||||
{
|
}
|
||||||
field.ViewportChanged -= OnViewportChanged;
|
|
||||||
}
|
field = value;
|
||||||
|
|
||||||
field = value;
|
if (field != null)
|
||||||
|
{
|
||||||
if (field != null)
|
field.ViewportChanged += OnViewportChanged;
|
||||||
{
|
|
||||||
field.ViewportChanged += OnViewportChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateData();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
|
||||||
{
|
|
||||||
UpdateData();
|
UpdateData();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void SetDataTransform(Matrix matrix)
|
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (Data.Transform is MatrixTransform transform
|
UpdateData();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SetDataTransform(Matrix matrix)
|
||||||
|
{
|
||||||
|
if (Data.Transform is MatrixTransform transform
|
||||||
#if WPF
|
#if WPF
|
||||||
&& !transform.IsFrozen
|
&& !transform.IsFrozen
|
||||||
#endif
|
#endif
|
||||||
)
|
)
|
||||||
{
|
|
||||||
transform.Matrix = matrix;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Data.Transform = new MatrixTransform { Matrix = matrix };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void UpdateData()
|
|
||||||
{
|
{
|
||||||
if (Data != null && ParentMap != null && Location != null)
|
transform.Matrix = matrix;
|
||||||
{
|
|
||||||
SetDataTransform(ParentMap.GetMapToViewTransform(Location));
|
|
||||||
}
|
|
||||||
|
|
||||||
MapPanel.SetLocation(this, Location);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
protected Point LocationToMap(Location location, double longitudeOffset)
|
|
||||||
{
|
{
|
||||||
var point = ParentMap.MapProjection.LocationToMap(location.Latitude, location.Longitude + longitudeOffset);
|
Data.Transform = new MatrixTransform { Matrix = matrix };
|
||||||
|
|
||||||
if (point.Y == double.PositiveInfinity)
|
|
||||||
{
|
|
||||||
point = new Point(point.X, 1e9);
|
|
||||||
}
|
|
||||||
else if (point.Y == double.NegativeInfinity)
|
|
||||||
{
|
|
||||||
point = new Point(point.X, -1e9);
|
|
||||||
}
|
|
||||||
|
|
||||||
return point;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Point LocationToView(Location location, double longitudeOffset)
|
|
||||||
{
|
|
||||||
return ParentMap.ViewTransform.MapToView(LocationToMap(location, longitudeOffset));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void UpdateData()
|
||||||
|
{
|
||||||
|
if (Data != null && ParentMap != null && Location != null)
|
||||||
|
{
|
||||||
|
SetDataTransform(ParentMap.GetMapToViewTransform(Location));
|
||||||
|
}
|
||||||
|
|
||||||
|
MapPanel.SetLocation(this, Location);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Point LocationToMap(Location location, double longitudeOffset)
|
||||||
|
{
|
||||||
|
var point = ParentMap.MapProjection.LocationToMap(location.Latitude, location.Longitude + longitudeOffset);
|
||||||
|
|
||||||
|
if (point.Y == double.PositiveInfinity)
|
||||||
|
{
|
||||||
|
point = new Point(point.X, 1e9);
|
||||||
|
}
|
||||||
|
else if (point.Y == double.NegativeInfinity)
|
||||||
|
{
|
||||||
|
point = new Point(point.X, -1e9);
|
||||||
|
}
|
||||||
|
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Point LocationToView(Location location, double longitudeOffset)
|
||||||
|
{
|
||||||
|
return ParentMap.ViewTransform.MapToView(LocationToMap(location, longitudeOffset));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,32 +7,31 @@ using Windows.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A polygon defined by a collection of Locations.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapPolygon : MapPolypoint
|
||||||
{
|
{
|
||||||
|
public static readonly DependencyProperty LocationsProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapPolygon, IEnumerable<Location>>(nameof(Locations), null,
|
||||||
|
(polygon, oldValue, newValue) => polygon.DataCollectionPropertyChanged(oldValue, newValue));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A polygon defined by a collection of Locations.
|
/// Gets or sets the Locations that define the polygon points.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MapPolygon : MapPolypoint
|
|
||||||
{
|
|
||||||
public static readonly DependencyProperty LocationsProperty =
|
|
||||||
DependencyPropertyHelper.Register<MapPolygon, IEnumerable<Location>>(nameof(Locations), null,
|
|
||||||
(polygon, oldValue, newValue) => polygon.DataCollectionPropertyChanged(oldValue, newValue));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the Locations that define the polygon points.
|
|
||||||
/// </summary>
|
|
||||||
#if WPF
|
#if WPF
|
||||||
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
||||||
#endif
|
#endif
|
||||||
public IEnumerable<Location> Locations
|
public IEnumerable<Location> Locations
|
||||||
{
|
{
|
||||||
get => (IEnumerable<Location>)GetValue(LocationsProperty);
|
get => (IEnumerable<Location>)GetValue(LocationsProperty);
|
||||||
set => SetValue(LocationsProperty, value);
|
set => SetValue(LocationsProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateData()
|
protected override void UpdateData()
|
||||||
{
|
{
|
||||||
UpdateData(Locations, true);
|
UpdateData(Locations, true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,32 +7,31 @@ using Windows.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A polyline defined by a collection of Locations.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapPolyline : MapPolypoint
|
||||||
{
|
{
|
||||||
|
public static readonly DependencyProperty LocationsProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapPolyline, IEnumerable<Location>>(nameof(Locations), null,
|
||||||
|
(polyline, oldValue, newValue) => polyline.DataCollectionPropertyChanged(oldValue, newValue));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A polyline defined by a collection of Locations.
|
/// Gets or sets the Locations that define the polyline points.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MapPolyline : MapPolypoint
|
|
||||||
{
|
|
||||||
public static readonly DependencyProperty LocationsProperty =
|
|
||||||
DependencyPropertyHelper.Register<MapPolyline, IEnumerable<Location>>(nameof(Locations), null,
|
|
||||||
(polyline, oldValue, newValue) => polyline.DataCollectionPropertyChanged(oldValue, newValue));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the Locations that define the polyline points.
|
|
||||||
/// </summary>
|
|
||||||
#if WPF
|
#if WPF
|
||||||
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
||||||
#endif
|
#endif
|
||||||
public IEnumerable<Location> Locations
|
public IEnumerable<Location> Locations
|
||||||
{
|
{
|
||||||
get => (IEnumerable<Location>)GetValue(LocationsProperty);
|
get => (IEnumerable<Location>)GetValue(LocationsProperty);
|
||||||
set => SetValue(LocationsProperty, value);
|
set => SetValue(LocationsProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateData()
|
protected override void UpdateData()
|
||||||
{
|
{
|
||||||
UpdateData(Locations, false);
|
UpdateData(Locations, false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,64 +20,63 @@ using Avalonia.Media;
|
||||||
using PolypointGeometry = Avalonia.Media.PathGeometry;
|
using PolypointGeometry = Avalonia.Media.PathGeometry;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class of MapPolyline and MapPolygon and MapMultiPolygon.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapPolypoint
|
||||||
{
|
{
|
||||||
/// <summary>
|
public static readonly DependencyProperty FillRuleProperty =
|
||||||
/// Base class of MapPolyline and MapPolygon and MapMultiPolygon.
|
DependencyPropertyHelper.Register<MapPolygon, FillRule>(nameof(FillRule), FillRule.EvenOdd,
|
||||||
/// </summary>
|
(polypoint, oldValue, newValue) => ((PolypointGeometry)polypoint.Data).FillRule = newValue);
|
||||||
public partial class MapPolypoint
|
|
||||||
|
public FillRule FillRule
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty FillRuleProperty =
|
get => (FillRule)GetValue(FillRuleProperty);
|
||||||
DependencyPropertyHelper.Register<MapPolygon, FillRule>(nameof(FillRule), FillRule.EvenOdd,
|
set => SetValue(FillRuleProperty, value);
|
||||||
(polypoint, oldValue, newValue) => ((PolypointGeometry)polypoint.Data).FillRule = newValue);
|
}
|
||||||
|
|
||||||
public FillRule FillRule
|
protected MapPolypoint()
|
||||||
|
{
|
||||||
|
Data = new PolypointGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void DataCollectionPropertyChanged(IEnumerable oldValue, IEnumerable newValue)
|
||||||
|
{
|
||||||
|
if (oldValue is INotifyCollectionChanged oldCollection)
|
||||||
{
|
{
|
||||||
get => (FillRule)GetValue(FillRuleProperty);
|
oldCollection.CollectionChanged -= DataCollectionChanged;
|
||||||
set => SetValue(FillRuleProperty, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected MapPolypoint()
|
if (newValue is INotifyCollectionChanged newCollection)
|
||||||
{
|
{
|
||||||
Data = new PolypointGeometry();
|
newCollection.CollectionChanged += DataCollectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void DataCollectionPropertyChanged(IEnumerable oldValue, IEnumerable newValue)
|
UpdateData();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void DataCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
UpdateData();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected double GetLongitudeOffset(IEnumerable<Location> locations)
|
||||||
|
{
|
||||||
|
var longitudeOffset = 0d;
|
||||||
|
|
||||||
|
if (ParentMap.MapProjection.IsNormalCylindrical)
|
||||||
{
|
{
|
||||||
if (oldValue is INotifyCollectionChanged oldCollection)
|
var location = Location ?? locations?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (location != null &&
|
||||||
|
!ParentMap.InsideViewBounds(ParentMap.LocationToView(location)))
|
||||||
{
|
{
|
||||||
oldCollection.CollectionChanged -= DataCollectionChanged;
|
longitudeOffset = ParentMap.NearestLongitude(location.Longitude) - location.Longitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValue is INotifyCollectionChanged newCollection)
|
|
||||||
{
|
|
||||||
newCollection.CollectionChanged += DataCollectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void DataCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
return longitudeOffset;
|
||||||
{
|
|
||||||
UpdateData();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected double GetLongitudeOffset(IEnumerable<Location> locations)
|
|
||||||
{
|
|
||||||
var longitudeOffset = 0d;
|
|
||||||
|
|
||||||
if (ParentMap.MapProjection.IsNormalCylindrical)
|
|
||||||
{
|
|
||||||
var location = Location ?? locations?.FirstOrDefault();
|
|
||||||
|
|
||||||
if (location != null &&
|
|
||||||
!ParentMap.InsideViewBounds(ParentMap.LocationToView(location)))
|
|
||||||
{
|
|
||||||
longitudeOffset = ParentMap.NearestLongitude(location.Longitude) - location.Longitude;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return longitudeOffset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,134 +6,133 @@ using System.Windows.Media;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements a map projection, a transformation between geographic coordinates,
|
/// Implements a map projection, a transformation between geographic coordinates,
|
||||||
/// i.e. latitude and longitude in degrees, and cartesian map coordinates in meters.
|
/// i.e. latitude and longitude in degrees, and cartesian map coordinates in meters.
|
||||||
/// See https://en.wikipedia.org/wiki/Map_projection.
|
/// See https://en.wikipedia.org/wiki/Map_projection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#if UWP || WINUI
|
#if UWP || WINUI
|
||||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||||
#else
|
#else
|
||||||
[System.ComponentModel.TypeConverter(typeof(MapProjectionConverter))]
|
[System.ComponentModel.TypeConverter(typeof(MapProjectionConverter))]
|
||||||
#endif
|
#endif
|
||||||
public abstract class MapProjection
|
public abstract class MapProjection
|
||||||
|
{
|
||||||
|
public const double Wgs84EquatorialRadius = 6378137d;
|
||||||
|
public const double Wgs84Flattening = 1d / 298.257223563;
|
||||||
|
public const double Wgs84MeterPerDegree = Wgs84EquatorialRadius * Math.PI / 180d;
|
||||||
|
|
||||||
|
public static MapProjectionFactory Factory
|
||||||
{
|
{
|
||||||
public const double Wgs84EquatorialRadius = 6378137d;
|
get => field ??= new MapProjectionFactory();
|
||||||
public const double Wgs84Flattening = 1d / 298.257223563;
|
set;
|
||||||
public const double Wgs84MeterPerDegree = Wgs84EquatorialRadius * Math.PI / 180d;
|
}
|
||||||
|
|
||||||
public static MapProjectionFactory Factory
|
/// <summary>
|
||||||
|
/// Creates a MapProjection instance from a CRS identifier string.
|
||||||
|
/// </summary>
|
||||||
|
public static MapProjection Parse(string crsId)
|
||||||
|
{
|
||||||
|
return Factory.GetProjection(crsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => CrsId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the WMS 1.3.0 CRS identifier.
|
||||||
|
/// </summary>
|
||||||
|
public string CrsId { get; protected set; }
|
||||||
|
|
||||||
|
public double EquatorialRadius { get; protected set; } = Wgs84EquatorialRadius;
|
||||||
|
public double Flattening { get; protected set; } = Wgs84Flattening;
|
||||||
|
public double ScaleFactor { get; protected set; } = 1d;
|
||||||
|
public double CentralMeridian { get; protected set; }
|
||||||
|
public double LatitudeOfOrigin { get; protected set; }
|
||||||
|
public double FalseEasting { get; protected set; }
|
||||||
|
public double FalseNorthing { get; protected set; }
|
||||||
|
public bool IsNormalCylindrical { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the grid convergence angle in degrees at the specified geographic coordinates.
|
||||||
|
/// Used for rotating the Rect resulting from BoundingBoxToMap in non-normal-cylindrical
|
||||||
|
/// projections, i.e. Transverse Mercator and Polar Stereographic.
|
||||||
|
/// </summary>
|
||||||
|
public virtual double GridConvergence(double latitude, double longitude) => 0d;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the relative transform at the specified geographic coordinates.
|
||||||
|
/// The returned Matrix represents the local relative scale and rotation.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Matrix RelativeTransform(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
var transform = new Matrix(ScaleFactor, 0d, 0d, ScaleFactor, 0d, 0d);
|
||||||
|
transform.Rotate(-GridConvergence(latitude, longitude));
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms geographic coordinates to a Point in projected map coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public abstract Point LocationToMap(double latitude, double longitude);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms projected map coordinates to a Location in geographic coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public abstract Location MapToLocation(double x, double y);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the relative transform at the specified geographic Location.
|
||||||
|
/// </summary>
|
||||||
|
public Matrix RelativeTransform(Location location) => RelativeTransform(location.Latitude, location.Longitude);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms a Location in geographic coordinates to a Point in projected map coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Point LocationToMap(Location location) => LocationToMap(location.Latitude, location.Longitude);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms a Point in projected map coordinates to a Location in geographic coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Location MapToLocation(Point point) => MapToLocation(point.X, point.Y);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms a BoundingBox in geographic coordinates to a Rect in projected map coordinates
|
||||||
|
/// with an optional rotation angle in degrees for non-normal-cylindrical projections.
|
||||||
|
/// </summary>
|
||||||
|
public (Rect, double) BoundingBoxToMap(BoundingBox boundingBox)
|
||||||
|
{
|
||||||
|
Rect rect;
|
||||||
|
var rotation = 0d;
|
||||||
|
var southWest = LocationToMap(boundingBox.South, boundingBox.West);
|
||||||
|
var northEast = LocationToMap(boundingBox.North, boundingBox.East);
|
||||||
|
|
||||||
|
if (IsNormalCylindrical)
|
||||||
{
|
{
|
||||||
get => field ??= new MapProjectionFactory();
|
rect = new Rect(southWest.X, southWest.Y, northEast.X - southWest.X, northEast.Y - southWest.Y);
|
||||||
set;
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var southEast = LocationToMap(boundingBox.South, boundingBox.East);
|
||||||
|
var northWest = LocationToMap(boundingBox.North, boundingBox.West);
|
||||||
|
var west = new Point((southWest.X + northWest.X) / 2d, (southWest.Y + northWest.Y) / 2d);
|
||||||
|
var east = new Point((southEast.X + northEast.X) / 2d, (southEast.Y + northEast.Y) / 2d);
|
||||||
|
var south = new Point((southWest.X + southEast.X) / 2d, (southWest.Y + southEast.Y) / 2d);
|
||||||
|
var north = new Point((northWest.X + northEast.X) / 2d, (northWest.Y + northEast.Y) / 2d);
|
||||||
|
var dxWidth = east.X - west.X;
|
||||||
|
var dyWidth = east.Y - west.Y;
|
||||||
|
var dxHeight = north.X - south.X;
|
||||||
|
var dyHeight = north.Y - south.Y;
|
||||||
|
var width = Math.Sqrt(dxWidth * dxWidth + dyWidth * dyWidth);
|
||||||
|
var height = Math.Sqrt(dxHeight * dxHeight + dyHeight * dyHeight);
|
||||||
|
var x = (south.X + north.X - width) / 2d; // cx-w/2
|
||||||
|
var y = (south.Y + north.Y - height) / 2d; // cy-h/2
|
||||||
|
|
||||||
|
rect = new Rect(x, y, width, height);
|
||||||
|
rotation = Math.Atan2(-dxHeight, dyHeight) * 180d / Math.PI;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return (rect, rotation);
|
||||||
/// Creates a MapProjection instance from a CRS identifier string.
|
|
||||||
/// </summary>
|
|
||||||
public static MapProjection Parse(string crsId)
|
|
||||||
{
|
|
||||||
return Factory.GetProjection(crsId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => CrsId;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the WMS 1.3.0 CRS identifier.
|
|
||||||
/// </summary>
|
|
||||||
public string CrsId { get; protected set; }
|
|
||||||
|
|
||||||
public double EquatorialRadius { get; protected set; } = Wgs84EquatorialRadius;
|
|
||||||
public double Flattening { get; protected set; } = Wgs84Flattening;
|
|
||||||
public double ScaleFactor { get; protected set; } = 1d;
|
|
||||||
public double CentralMeridian { get; protected set; }
|
|
||||||
public double LatitudeOfOrigin { get; protected set; }
|
|
||||||
public double FalseEasting { get; protected set; }
|
|
||||||
public double FalseNorthing { get; protected set; }
|
|
||||||
public bool IsNormalCylindrical { get; protected set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the grid convergence angle in degrees at the specified geographic coordinates.
|
|
||||||
/// Used for rotating the Rect resulting from BoundingBoxToMap in non-normal-cylindrical
|
|
||||||
/// projections, i.e. Transverse Mercator and Polar Stereographic.
|
|
||||||
/// </summary>
|
|
||||||
public virtual double GridConvergence(double latitude, double longitude) => 0d;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the relative transform at the specified geographic coordinates.
|
|
||||||
/// The returned Matrix represents the local relative scale and rotation.
|
|
||||||
/// </summary>
|
|
||||||
public virtual Matrix RelativeTransform(double latitude, double longitude)
|
|
||||||
{
|
|
||||||
var transform = new Matrix(ScaleFactor, 0d, 0d, ScaleFactor, 0d, 0d);
|
|
||||||
transform.Rotate(-GridConvergence(latitude, longitude));
|
|
||||||
return transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transforms geographic coordinates to a Point in projected map coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public abstract Point LocationToMap(double latitude, double longitude);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transforms projected map coordinates to a Location in geographic coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public abstract Location MapToLocation(double x, double y);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the relative transform at the specified geographic Location.
|
|
||||||
/// </summary>
|
|
||||||
public Matrix RelativeTransform(Location location) => RelativeTransform(location.Latitude, location.Longitude);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transforms a Location in geographic coordinates to a Point in projected map coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public Point LocationToMap(Location location) => LocationToMap(location.Latitude, location.Longitude);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transforms a Point in projected map coordinates to a Location in geographic coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public Location MapToLocation(Point point) => MapToLocation(point.X, point.Y);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transforms a BoundingBox in geographic coordinates to a Rect in projected map coordinates
|
|
||||||
/// with an optional rotation angle in degrees for non-normal-cylindrical projections.
|
|
||||||
/// </summary>
|
|
||||||
public (Rect, double) BoundingBoxToMap(BoundingBox boundingBox)
|
|
||||||
{
|
|
||||||
Rect rect;
|
|
||||||
var rotation = 0d;
|
|
||||||
var southWest = LocationToMap(boundingBox.South, boundingBox.West);
|
|
||||||
var northEast = LocationToMap(boundingBox.North, boundingBox.East);
|
|
||||||
|
|
||||||
if (IsNormalCylindrical)
|
|
||||||
{
|
|
||||||
rect = new Rect(southWest.X, southWest.Y, northEast.X - southWest.X, northEast.Y - southWest.Y);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var southEast = LocationToMap(boundingBox.South, boundingBox.East);
|
|
||||||
var northWest = LocationToMap(boundingBox.North, boundingBox.West);
|
|
||||||
var west = new Point((southWest.X + northWest.X) / 2d, (southWest.Y + northWest.Y) / 2d);
|
|
||||||
var east = new Point((southEast.X + northEast.X) / 2d, (southEast.Y + northEast.Y) / 2d);
|
|
||||||
var south = new Point((southWest.X + southEast.X) / 2d, (southWest.Y + southEast.Y) / 2d);
|
|
||||||
var north = new Point((northWest.X + northEast.X) / 2d, (northWest.Y + northEast.Y) / 2d);
|
|
||||||
var dxWidth = east.X - west.X;
|
|
||||||
var dyWidth = east.Y - west.Y;
|
|
||||||
var dxHeight = north.X - south.X;
|
|
||||||
var dyHeight = north.Y - south.Y;
|
|
||||||
var width = Math.Sqrt(dxWidth * dxWidth + dyWidth * dyWidth);
|
|
||||||
var height = Math.Sqrt(dxHeight * dxHeight + dyHeight * dyHeight);
|
|
||||||
var x = (south.X + north.X - width) / 2d; // cx-w/2
|
|
||||||
var y = (south.Y + north.Y - height) / 2d; // cy-h/2
|
|
||||||
|
|
||||||
rect = new Rect(x, y, width, height);
|
|
||||||
rotation = Math.Atan2(-dxHeight, dyHeight) * 180d / Math.PI;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (rect, rotation);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,56 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class MapProjectionFactory
|
||||||
{
|
{
|
||||||
public class MapProjectionFactory
|
public MapProjection GetProjection(string crsId)
|
||||||
{
|
{
|
||||||
public MapProjection GetProjection(string crsId)
|
var projection = CreateProjection(crsId);
|
||||||
|
|
||||||
|
if (projection == null &&
|
||||||
|
crsId.StartsWith("EPSG:") &&
|
||||||
|
int.TryParse(crsId.Substring(5), out int epsgCode))
|
||||||
{
|
{
|
||||||
var projection = CreateProjection(crsId);
|
projection = CreateProjection(epsgCode);
|
||||||
|
|
||||||
if (projection == null &&
|
|
||||||
crsId.StartsWith("EPSG:") &&
|
|
||||||
int.TryParse(crsId.Substring(5), out int epsgCode))
|
|
||||||
{
|
|
||||||
projection = CreateProjection(epsgCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
return projection ?? throw new NotSupportedException($"MapProjection \"{crsId}\" is not supported.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual MapProjection CreateProjection(string crsId)
|
return projection ?? throw new NotSupportedException($"MapProjection \"{crsId}\" is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual MapProjection CreateProjection(string crsId)
|
||||||
|
{
|
||||||
|
MapProjection projection = crsId switch
|
||||||
{
|
{
|
||||||
MapProjection projection = crsId switch
|
WebMercatorProjection.DefaultCrsId => new WebMercatorProjection(),
|
||||||
{
|
WorldMercatorProjection.DefaultCrsId => new WorldMercatorProjection(),
|
||||||
WebMercatorProjection.DefaultCrsId => new WebMercatorProjection(),
|
Wgs84UpsNorthProjection.DefaultCrsId => new Wgs84UpsNorthProjection(),
|
||||||
WorldMercatorProjection.DefaultCrsId => new WorldMercatorProjection(),
|
Wgs84UpsSouthProjection.DefaultCrsId => new Wgs84UpsSouthProjection(),
|
||||||
Wgs84UpsNorthProjection.DefaultCrsId => new Wgs84UpsNorthProjection(),
|
EquirectangularProjection.DefaultCrsId or "CRS:84" => new EquirectangularProjection(crsId),
|
||||||
Wgs84UpsSouthProjection.DefaultCrsId => new Wgs84UpsSouthProjection(),
|
_ => null
|
||||||
EquirectangularProjection.DefaultCrsId or "CRS:84" => new EquirectangularProjection(crsId),
|
};
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (projection == null && crsId.StartsWith(StereographicProjection.DefaultCrsId))
|
if (projection == null && crsId.StartsWith(StereographicProjection.DefaultCrsId))
|
||||||
{
|
{
|
||||||
projection = new StereographicProjection(crsId);
|
projection = new StereographicProjection(crsId);
|
||||||
}
|
|
||||||
|
|
||||||
return projection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual MapProjection CreateProjection(int epsgCode)
|
return projection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual MapProjection CreateProjection(int epsgCode)
|
||||||
|
{
|
||||||
|
return epsgCode switch
|
||||||
{
|
{
|
||||||
return epsgCode switch
|
var c when c is >= Etrs89UtmProjection.FirstZoneEpsgCode
|
||||||
{
|
and <= Etrs89UtmProjection.LastZoneEpsgCode => new Etrs89UtmProjection(c % 100),
|
||||||
var c when c is >= Etrs89UtmProjection.FirstZoneEpsgCode
|
var c when c is >= Nad83UtmProjection.FirstZoneEpsgCode
|
||||||
and <= Etrs89UtmProjection.LastZoneEpsgCode => new Etrs89UtmProjection(c % 100),
|
and <= Nad83UtmProjection.LastZoneEpsgCode => new Nad83UtmProjection(c % 100),
|
||||||
var c when c is >= Nad83UtmProjection.FirstZoneEpsgCode
|
var c when c is >= Wgs84UtmProjection.FirstZoneNorthEpsgCode
|
||||||
and <= Nad83UtmProjection.LastZoneEpsgCode => new Nad83UtmProjection(c % 100),
|
and <= Wgs84UtmProjection.LastZoneNorthEpsgCode => new Wgs84UtmProjection(c % 100, true),
|
||||||
var c when c is >= Wgs84UtmProjection.FirstZoneNorthEpsgCode
|
var c when c is >= Wgs84UtmProjection.FirstZoneSouthEpsgCode
|
||||||
and <= Wgs84UtmProjection.LastZoneNorthEpsgCode => new Wgs84UtmProjection(c % 100, true),
|
and <= Wgs84UtmProjection.LastZoneSouthEpsgCode => new Wgs84UtmProjection(c % 100, false),
|
||||||
var c when c is >= Wgs84UtmProjection.FirstZoneSouthEpsgCode
|
_ => null
|
||||||
and <= Wgs84UtmProjection.LastZoneSouthEpsgCode => new Wgs84UtmProjection(c % 100, false),
|
};
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,105 +26,104 @@ using Avalonia.Layout;
|
||||||
using PropertyPath = System.String;
|
using PropertyPath = System.String;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws a map scale overlay.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapScale : MapPanel
|
||||||
{
|
{
|
||||||
/// <summary>
|
public static readonly DependencyProperty PaddingProperty =
|
||||||
/// Draws a map scale overlay.
|
DependencyPropertyHelper.Register<MapScale, Thickness>(nameof(Padding), new Thickness(4));
|
||||||
/// </summary>
|
|
||||||
public partial class MapScale : MapPanel
|
public static readonly DependencyProperty StrokeThicknessProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapScale, double>(nameof(StrokeThickness), 1d);
|
||||||
|
|
||||||
|
public Thickness Padding
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty PaddingProperty =
|
get => (Thickness)GetValue(PaddingProperty);
|
||||||
DependencyPropertyHelper.Register<MapScale, Thickness>(nameof(Padding), new Thickness(4));
|
set => SetValue(PaddingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty StrokeThicknessProperty =
|
public double StrokeThickness
|
||||||
DependencyPropertyHelper.Register<MapScale, double>(nameof(StrokeThickness), 1d);
|
{
|
||||||
|
get => (double)GetValue(StrokeThicknessProperty);
|
||||||
|
set => SetValue(StrokeThicknessProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public Thickness Padding
|
private readonly Polyline line = new Polyline();
|
||||||
{
|
|
||||||
get => (Thickness)GetValue(PaddingProperty);
|
|
||||||
set => SetValue(PaddingProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double StrokeThickness
|
private readonly TextBlock label = new TextBlock
|
||||||
{
|
{
|
||||||
get => (double)GetValue(StrokeThicknessProperty);
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
set => SetValue(StrokeThicknessProperty, value);
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
}
|
};
|
||||||
|
|
||||||
private readonly Polyline line = new Polyline();
|
public MapScale()
|
||||||
|
{
|
||||||
|
MinWidth = 100d;
|
||||||
|
Children.Add(line);
|
||||||
|
Children.Add(label);
|
||||||
|
}
|
||||||
|
|
||||||
private readonly TextBlock label = new TextBlock
|
protected override void SetParentMap(MapBase map)
|
||||||
{
|
{
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
base.SetParentMap(map);
|
||||||
VerticalAlignment = VerticalAlignment.Center
|
|
||||||
};
|
|
||||||
|
|
||||||
public MapScale()
|
line.SetBinding(Shape.StrokeThicknessProperty,
|
||||||
{
|
new Binding { Source = this, Path = new PropertyPath(nameof(StrokeThickness)) });
|
||||||
MinWidth = 100d;
|
|
||||||
Children.Add(line);
|
|
||||||
Children.Add(label);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void SetParentMap(MapBase map)
|
line.SetBinding(Shape.StrokeProperty,
|
||||||
{
|
new Binding { Source = map, Path = new PropertyPath(nameof(MapBase.Foreground)) });
|
||||||
base.SetParentMap(map);
|
|
||||||
|
|
||||||
line.SetBinding(Shape.StrokeThicknessProperty,
|
|
||||||
new Binding { Source = this, Path = new PropertyPath(nameof(StrokeThickness)) });
|
|
||||||
|
|
||||||
line.SetBinding(Shape.StrokeProperty,
|
|
||||||
new Binding { Source = map, Path = new PropertyPath(nameof(MapBase.Foreground)) });
|
|
||||||
|
|
||||||
#if UWP || WINUI
|
#if UWP || WINUI
|
||||||
label.SetBinding(TextBlock.ForegroundProperty,
|
label.SetBinding(TextBlock.ForegroundProperty,
|
||||||
new Binding { Source = map, Path = new PropertyPath(nameof(MapBase.Foreground)) });
|
new Binding { Source = map, Path = new PropertyPath(nameof(MapBase.Foreground)) });
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Size MeasureOverride(Size availableSize)
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
|
{
|
||||||
|
var size = new Size();
|
||||||
|
|
||||||
|
if (ParentMap != null)
|
||||||
{
|
{
|
||||||
var size = new Size();
|
var x0 = ParentMap.ActualWidth / 2d;
|
||||||
|
var y0 = ParentMap.ActualHeight / 2d;
|
||||||
|
var p1 = ParentMap.ViewToLocation(new Point(x0 - 50d, y0));
|
||||||
|
var p2 = ParentMap.ViewToLocation(new Point(x0 + 50d, y0));
|
||||||
|
var scale = 100d / p1.GetDistance(p2);
|
||||||
|
var length = MinWidth / scale;
|
||||||
|
var magnitude = Math.Pow(10d, Math.Floor(Math.Log10(length)));
|
||||||
|
|
||||||
if (ParentMap != null)
|
length = length / magnitude < 2d ? 2d * magnitude
|
||||||
{
|
: length / magnitude < 5d ? 5d * magnitude
|
||||||
var x0 = ParentMap.ActualWidth / 2d;
|
: 10d * magnitude;
|
||||||
var y0 = ParentMap.ActualHeight / 2d;
|
|
||||||
var p1 = ParentMap.ViewToLocation(new Point(x0 - 50d, y0));
|
|
||||||
var p2 = ParentMap.ViewToLocation(new Point(x0 + 50d, y0));
|
|
||||||
var scale = 100d / p1.GetDistance(p2);
|
|
||||||
var length = MinWidth / scale;
|
|
||||||
var magnitude = Math.Pow(10d, Math.Floor(Math.Log10(length)));
|
|
||||||
|
|
||||||
length = length / magnitude < 2d ? 2d * magnitude
|
size = new Size(
|
||||||
: length / magnitude < 5d ? 5d * magnitude
|
length * scale + StrokeThickness + Padding.Left + Padding.Right,
|
||||||
: 10d * magnitude;
|
1.5 * label.FontSize + 2 * StrokeThickness + Padding.Top + Padding.Bottom);
|
||||||
|
|
||||||
size = new Size(
|
var x1 = Padding.Left + StrokeThickness / 2d;
|
||||||
length * scale + StrokeThickness + Padding.Left + Padding.Right,
|
var x2 = size.Width - Padding.Right - StrokeThickness / 2d;
|
||||||
1.5 * label.FontSize + 2 * StrokeThickness + Padding.Top + Padding.Bottom);
|
var y1 = size.Height / 2d;
|
||||||
|
var y2 = size.Height - Padding.Bottom - StrokeThickness / 2d;
|
||||||
|
|
||||||
var x1 = Padding.Left + StrokeThickness / 2d;
|
line.Points = [new Point(x1, y1), new Point(x1, y2), new Point(x2, y2), new Point(x2, y1)];
|
||||||
var x2 = size.Width - Padding.Right - StrokeThickness / 2d;
|
|
||||||
var y1 = size.Height / 2d;
|
|
||||||
var y2 = size.Height - Padding.Bottom - StrokeThickness / 2d;
|
|
||||||
|
|
||||||
line.Points = [new Point(x1, y1), new Point(x1, y2), new Point(x2, y2), new Point(x2, y1)];
|
label.Text = length >= 1000d
|
||||||
|
? string.Format(CultureInfo.InvariantCulture, "{0:F0} km", length / 1000d)
|
||||||
|
: string.Format(CultureInfo.InvariantCulture, "{0:F0} m", length);
|
||||||
|
|
||||||
label.Text = length >= 1000d
|
line.Measure(size);
|
||||||
? string.Format(CultureInfo.InvariantCulture, "{0:F0} km", length / 1000d)
|
label.Measure(size);
|
||||||
: string.Format(CultureInfo.InvariantCulture, "{0:F0} m", length);
|
|
||||||
|
|
||||||
line.Measure(size);
|
|
||||||
label.Measure(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
return size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnViewportChanged(ViewportChangedEventArgs e)
|
return size;
|
||||||
{
|
}
|
||||||
InvalidateMeasure();
|
|
||||||
}
|
protected override void OnViewportChanged(ViewportChangedEventArgs e)
|
||||||
|
{
|
||||||
|
InvalidateMeasure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,226 +16,225 @@ using Avalonia;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Displays a Web Mercator tile pyramid.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MapTileLayer : TilePyramidLayer
|
||||||
{
|
{
|
||||||
|
private const int TileSize = 256;
|
||||||
|
|
||||||
|
private static readonly Point MapTopLeft = new Point(-180d * MapProjection.Wgs84MeterPerDegree,
|
||||||
|
180d * MapProjection.Wgs84MeterPerDegree);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty TileSourceProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapTileLayer, TileSource>(nameof(TileSource), null,
|
||||||
|
(layer, oldValue, newValue) => layer.UpdateTileCollection(true));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MinZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapTileLayer, int>(nameof(MinZoomLevel), 0);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MaxZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapTileLayer, int>(nameof(MaxZoomLevel), 19);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ZoomLevelOffsetProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapTileLayer, double>(nameof(ZoomLevelOffset), 0d);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Displays a Web Mercator tile pyramid.
|
/// A default MapTileLayer using OpenStreetMap data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MapTileLayer : TilePyramidLayer
|
public static MapTileLayer OpenStreetMapTileLayer => new MapTileLayer
|
||||||
{
|
{
|
||||||
private const int TileSize = 256;
|
TileSource = TileSource.Parse("https://tile.openstreetmap.org/{z}/{x}/{y}.png"),
|
||||||
|
SourceName = "OpenStreetMap",
|
||||||
|
Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)"
|
||||||
|
};
|
||||||
|
|
||||||
private static readonly Point MapTopLeft = new Point(-180d * MapProjection.Wgs84MeterPerDegree,
|
public MapTileLayer()
|
||||||
180d * MapProjection.Wgs84MeterPerDegree);
|
{
|
||||||
|
this.SetRenderTransform(new MatrixTransform());
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty TileSourceProperty =
|
public TileMatrix TileMatrix { get; private set; }
|
||||||
DependencyPropertyHelper.Register<MapTileLayer, TileSource>(nameof(TileSource), null,
|
|
||||||
(layer, oldValue, newValue) => layer.UpdateTileCollection(true));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MinZoomLevelProperty =
|
public ICollection<ImageTile> Tiles { get; private set; } = [];
|
||||||
DependencyPropertyHelper.Register<MapTileLayer, int>(nameof(MinZoomLevel), 0);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MaxZoomLevelProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MapTileLayer, int>(nameof(MaxZoomLevel), 19);
|
/// Provides the ImagesSource or image request Uri for map tiles.
|
||||||
|
/// </summary>
|
||||||
|
public TileSource TileSource
|
||||||
|
{
|
||||||
|
get => (TileSource)GetValue(TileSourceProperty);
|
||||||
|
set => SetValue(TileSourceProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty ZoomLevelOffsetProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MapTileLayer, double>(nameof(ZoomLevelOffset), 0d);
|
/// Minimum zoom level supported by the MapTileLayer. Default value is 0.
|
||||||
|
/// </summary>
|
||||||
|
public int MinZoomLevel
|
||||||
|
{
|
||||||
|
get => (int)GetValue(MinZoomLevelProperty);
|
||||||
|
set => SetValue(MinZoomLevelProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A default MapTileLayer using OpenStreetMap data.
|
/// Maximum zoom level supported by the MapTileLayer. Default value is 19.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static MapTileLayer OpenStreetMapTileLayer => new MapTileLayer
|
public int MaxZoomLevel
|
||||||
|
{
|
||||||
|
get => (int)GetValue(MaxZoomLevelProperty);
|
||||||
|
set => SetValue(MaxZoomLevelProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional offset between the map zoom level and the topmost tile zoom level.
|
||||||
|
/// Default value is 0.
|
||||||
|
/// </summary>
|
||||||
|
public double ZoomLevelOffset
|
||||||
|
{
|
||||||
|
get => (double)GetValue(ZoomLevelOffsetProperty);
|
||||||
|
set => SetValue(ZoomLevelOffsetProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
|
{
|
||||||
|
foreach (var tile in Tiles)
|
||||||
{
|
{
|
||||||
TileSource = TileSource.Parse("https://tile.openstreetmap.org/{z}/{x}/{y}.png"),
|
tile.Image.Measure(availableSize);
|
||||||
SourceName = "OpenStreetMap",
|
|
||||||
Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)"
|
|
||||||
};
|
|
||||||
|
|
||||||
public MapTileLayer()
|
|
||||||
{
|
|
||||||
this.SetRenderTransform(new MatrixTransform());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TileMatrix TileMatrix { get; private set; }
|
return new Size();
|
||||||
|
}
|
||||||
|
|
||||||
public ICollection<ImageTile> Tiles { get; private set; } = [];
|
protected override Size ArrangeOverride(Size finalSize)
|
||||||
|
{
|
||||||
/// <summary>
|
foreach (var tile in Tiles)
|
||||||
/// Provides the ImagesSource or image request Uri for map tiles.
|
|
||||||
/// </summary>
|
|
||||||
public TileSource TileSource
|
|
||||||
{
|
{
|
||||||
get => (TileSource)GetValue(TileSourceProperty);
|
// Arrange tiles relative to TileMatrix.XMin/YMin.
|
||||||
set => SetValue(TileSourceProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Minimum zoom level supported by the MapTileLayer. Default value is 0.
|
|
||||||
/// </summary>
|
|
||||||
public int MinZoomLevel
|
|
||||||
{
|
|
||||||
get => (int)GetValue(MinZoomLevelProperty);
|
|
||||||
set => SetValue(MinZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum zoom level supported by the MapTileLayer. Default value is 19.
|
|
||||||
/// </summary>
|
|
||||||
public int MaxZoomLevel
|
|
||||||
{
|
|
||||||
get => (int)GetValue(MaxZoomLevelProperty);
|
|
||||||
set => SetValue(MaxZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional offset between the map zoom level and the topmost tile zoom level.
|
|
||||||
/// Default value is 0.
|
|
||||||
/// </summary>
|
|
||||||
public double ZoomLevelOffset
|
|
||||||
{
|
|
||||||
get => (double)GetValue(ZoomLevelOffsetProperty);
|
|
||||||
set => SetValue(ZoomLevelOffsetProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Size MeasureOverride(Size availableSize)
|
|
||||||
{
|
|
||||||
foreach (var tile in Tiles)
|
|
||||||
{
|
|
||||||
tile.Image.Measure(availableSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Size();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Size ArrangeOverride(Size finalSize)
|
|
||||||
{
|
|
||||||
foreach (var tile in Tiles)
|
|
||||||
{
|
|
||||||
// Arrange tiles relative to TileMatrix.XMin/YMin.
|
|
||||||
//
|
|
||||||
var tileSize = TileSize << (TileMatrix.ZoomLevel - tile.ZoomLevel);
|
|
||||||
var x = tileSize * tile.X - TileSize * TileMatrix.XMin;
|
|
||||||
var y = tileSize * tile.Y - TileSize * TileMatrix.YMin;
|
|
||||||
|
|
||||||
tile.Image.Width = tileSize;
|
|
||||||
tile.Image.Height = tileSize;
|
|
||||||
tile.Image.Arrange(new Rect(x, y, tileSize, tileSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateRenderTransform()
|
|
||||||
{
|
|
||||||
if (TileMatrix != null)
|
|
||||||
{
|
|
||||||
// Tile matrix origin in pixels.
|
|
||||||
//
|
|
||||||
var tileMatrixOrigin = new Point(TileSize * TileMatrix.XMin, TileSize * TileMatrix.YMin);
|
|
||||||
var tileMatrixScale = MapBase.ZoomLevelToScale(TileMatrix.ZoomLevel);
|
|
||||||
|
|
||||||
((MatrixTransform)RenderTransform).Matrix =
|
|
||||||
ParentMap.ViewTransform.GetTileLayerTransform(tileMatrixScale, MapTopLeft, tileMatrixOrigin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateTileCollection()
|
|
||||||
{
|
|
||||||
UpdateTileCollection(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateTileCollection(bool tileSourceChanged)
|
|
||||||
{
|
|
||||||
if (TileSource == null || ParentMap?.MapProjection.CrsId != WebMercatorProjection.DefaultCrsId)
|
|
||||||
{
|
|
||||||
CancelLoadTiles();
|
|
||||||
Children.Clear();
|
|
||||||
Tiles.Clear();
|
|
||||||
TileMatrix = null;
|
|
||||||
}
|
|
||||||
else if (SetTileMatrix() || tileSourceChanged)
|
|
||||||
{
|
|
||||||
if (tileSourceChanged)
|
|
||||||
{
|
|
||||||
Tiles.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateRenderTransform();
|
|
||||||
UpdateTiles();
|
|
||||||
BeginLoadTiles(Tiles, TileSource, SourceName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool SetTileMatrix()
|
|
||||||
{
|
|
||||||
// Add 0.001 to avoid floating point precision.
|
|
||||||
//
|
//
|
||||||
var tileMatrixZoomLevel = (int)Math.Floor(ParentMap.ZoomLevel - ZoomLevelOffset + 0.001);
|
var tileSize = TileSize << (TileMatrix.ZoomLevel - tile.ZoomLevel);
|
||||||
var tileMatrixScale = MapBase.ZoomLevelToScale(tileMatrixZoomLevel);
|
var x = tileSize * tile.X - TileSize * TileMatrix.XMin;
|
||||||
|
var y = tileSize * tile.Y - TileSize * TileMatrix.YMin;
|
||||||
|
|
||||||
// Tile matrix bounds in pixels.
|
tile.Image.Width = tileSize;
|
||||||
//
|
tile.Image.Height = tileSize;
|
||||||
var bounds = ParentMap.ViewTransform.GetTileMatrixBounds(tileMatrixScale, MapTopLeft, ParentMap.ActualWidth, ParentMap.ActualHeight);
|
tile.Image.Arrange(new Rect(x, y, tileSize, tileSize));
|
||||||
|
|
||||||
// Tile X and Y bounds.
|
|
||||||
//
|
|
||||||
var xMin = (int)Math.Floor(bounds.X / TileSize);
|
|
||||||
var yMin = (int)Math.Floor(bounds.Y / TileSize);
|
|
||||||
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / TileSize);
|
|
||||||
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / TileSize);
|
|
||||||
|
|
||||||
if (TileMatrix != null &&
|
|
||||||
TileMatrix.ZoomLevel == tileMatrixZoomLevel &&
|
|
||||||
TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
|
|
||||||
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
TileMatrix = new TileMatrix(tileMatrixZoomLevel, xMin, yMin, xMax, yMax);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateTiles()
|
return finalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateRenderTransform()
|
||||||
|
{
|
||||||
|
if (TileMatrix != null)
|
||||||
{
|
{
|
||||||
var tiles = new ImageTileList();
|
// Tile matrix origin in pixels.
|
||||||
var maxZoomLevel = Math.Min(TileMatrix.ZoomLevel, MaxZoomLevel);
|
//
|
||||||
|
var tileMatrixOrigin = new Point(TileSize * TileMatrix.XMin, TileSize * TileMatrix.YMin);
|
||||||
|
var tileMatrixScale = MapBase.ZoomLevelToScale(TileMatrix.ZoomLevel);
|
||||||
|
|
||||||
if (maxZoomLevel >= MinZoomLevel)
|
((MatrixTransform)RenderTransform).Matrix =
|
||||||
{
|
ParentMap.ViewTransform.GetTileLayerTransform(tileMatrixScale, MapTopLeft, tileMatrixOrigin);
|
||||||
var minZoomLevel = maxZoomLevel;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (IsBaseMapLayer)
|
protected override void UpdateTileCollection()
|
||||||
{
|
{
|
||||||
var bgLevels = Math.Max(MaxBackgroundLevels, 0);
|
UpdateTileCollection(false);
|
||||||
minZoomLevel = Math.Max(TileMatrix.ZoomLevel - bgLevels, MinZoomLevel);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (var zoomLevel = minZoomLevel; zoomLevel <= maxZoomLevel; zoomLevel++)
|
|
||||||
{
|
|
||||||
var tileCount = 1 << zoomLevel; // per row and column
|
|
||||||
|
|
||||||
// Right-shift divides with rounding down also negative values, https://stackoverflow.com/q/55196178
|
|
||||||
//
|
|
||||||
var shift = TileMatrix.ZoomLevel - zoomLevel;
|
|
||||||
var xMin = TileMatrix.XMin >> shift; // may be < 0
|
|
||||||
var xMax = TileMatrix.XMax >> shift; // may be >= tileCount
|
|
||||||
var yMin = Math.Max(TileMatrix.YMin >> shift, 0);
|
|
||||||
var yMax = Math.Min(TileMatrix.YMax >> shift, tileCount - 1);
|
|
||||||
|
|
||||||
tiles.FillMatrix(Tiles, zoomLevel, xMin, yMin, xMax, yMax, tileCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Tiles = tiles;
|
|
||||||
|
|
||||||
|
private void UpdateTileCollection(bool tileSourceChanged)
|
||||||
|
{
|
||||||
|
if (TileSource == null || ParentMap?.MapProjection.CrsId != WebMercatorProjection.DefaultCrsId)
|
||||||
|
{
|
||||||
|
CancelLoadTiles();
|
||||||
Children.Clear();
|
Children.Clear();
|
||||||
|
Tiles.Clear();
|
||||||
foreach (var tile in tiles)
|
TileMatrix = null;
|
||||||
|
}
|
||||||
|
else if (SetTileMatrix() || tileSourceChanged)
|
||||||
|
{
|
||||||
|
if (tileSourceChanged)
|
||||||
{
|
{
|
||||||
Children.Add(tile.Image);
|
Tiles.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateRenderTransform();
|
||||||
|
UpdateTiles();
|
||||||
|
BeginLoadTiles(Tiles, TileSource, SourceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SetTileMatrix()
|
||||||
|
{
|
||||||
|
// Add 0.001 to avoid floating point precision.
|
||||||
|
//
|
||||||
|
var tileMatrixZoomLevel = (int)Math.Floor(ParentMap.ZoomLevel - ZoomLevelOffset + 0.001);
|
||||||
|
var tileMatrixScale = MapBase.ZoomLevelToScale(tileMatrixZoomLevel);
|
||||||
|
|
||||||
|
// Tile matrix bounds in pixels.
|
||||||
|
//
|
||||||
|
var bounds = ParentMap.ViewTransform.GetTileMatrixBounds(tileMatrixScale, MapTopLeft, ParentMap.ActualWidth, ParentMap.ActualHeight);
|
||||||
|
|
||||||
|
// Tile X and Y bounds.
|
||||||
|
//
|
||||||
|
var xMin = (int)Math.Floor(bounds.X / TileSize);
|
||||||
|
var yMin = (int)Math.Floor(bounds.Y / TileSize);
|
||||||
|
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / TileSize);
|
||||||
|
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / TileSize);
|
||||||
|
|
||||||
|
if (TileMatrix != null &&
|
||||||
|
TileMatrix.ZoomLevel == tileMatrixZoomLevel &&
|
||||||
|
TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
|
||||||
|
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TileMatrix = new TileMatrix(tileMatrixZoomLevel, xMin, yMin, xMax, yMax);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTiles()
|
||||||
|
{
|
||||||
|
var tiles = new ImageTileList();
|
||||||
|
var maxZoomLevel = Math.Min(TileMatrix.ZoomLevel, MaxZoomLevel);
|
||||||
|
|
||||||
|
if (maxZoomLevel >= MinZoomLevel)
|
||||||
|
{
|
||||||
|
var minZoomLevel = maxZoomLevel;
|
||||||
|
|
||||||
|
if (IsBaseMapLayer)
|
||||||
|
{
|
||||||
|
var bgLevels = Math.Max(MaxBackgroundLevels, 0);
|
||||||
|
minZoomLevel = Math.Max(TileMatrix.ZoomLevel - bgLevels, MinZoomLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var zoomLevel = minZoomLevel; zoomLevel <= maxZoomLevel; zoomLevel++)
|
||||||
|
{
|
||||||
|
var tileCount = 1 << zoomLevel; // per row and column
|
||||||
|
|
||||||
|
// Right-shift divides with rounding down also negative values, https://stackoverflow.com/q/55196178
|
||||||
|
//
|
||||||
|
var shift = TileMatrix.ZoomLevel - zoomLevel;
|
||||||
|
var xMin = TileMatrix.XMin >> shift; // may be < 0
|
||||||
|
var xMax = TileMatrix.XMax >> shift; // may be >= tileCount
|
||||||
|
var yMin = Math.Max(TileMatrix.YMin >> shift, 0);
|
||||||
|
var yMax = Math.Min(TileMatrix.YMax >> shift, tileCount - 1);
|
||||||
|
|
||||||
|
tiles.FillMatrix(Tiles, zoomLevel, xMin, yMin, xMax, yMax, tileCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Tiles = tiles;
|
||||||
|
|
||||||
|
Children.Clear();
|
||||||
|
|
||||||
|
foreach (var tile in tiles)
|
||||||
|
{
|
||||||
|
Children.Add(tile.Image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,112 +8,111 @@ using Avalonia;
|
||||||
using FrameworkMatrix = Avalonia.Matrix;
|
using FrameworkMatrix = Avalonia.Matrix;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces Windows.UI.Xaml.Media.Matrix, Microsoft.UI.Xaml.Media.Matrix and Avalonia.Matrix
|
||||||
|
/// to expose Translate, Rotate and Invert methods.
|
||||||
|
/// </summary>
|
||||||
|
public struct Matrix(double m11, double m12, double m21, double m22, double offsetX, double offsetY)
|
||||||
{
|
{
|
||||||
/// <summary>
|
public double M11 { get; private set; } = m11;
|
||||||
/// Replaces Windows.UI.Xaml.Media.Matrix, Microsoft.UI.Xaml.Media.Matrix and Avalonia.Matrix
|
public double M12 { get; private set; } = m12;
|
||||||
/// to expose Translate, Rotate and Invert methods.
|
public double M21 { get; private set; } = m21;
|
||||||
/// </summary>
|
public double M22 { get; private set; } = m22;
|
||||||
public struct Matrix(double m11, double m12, double m21, double m22, double offsetX, double offsetY)
|
public double OffsetX { get; private set; } = offsetX;
|
||||||
|
public double OffsetY { get; private set; } = offsetY;
|
||||||
|
|
||||||
|
public static implicit operator Matrix(FrameworkMatrix m)
|
||||||
{
|
{
|
||||||
public double M11 { get; private set; } = m11;
|
|
||||||
public double M12 { get; private set; } = m12;
|
|
||||||
public double M21 { get; private set; } = m21;
|
|
||||||
public double M22 { get; private set; } = m22;
|
|
||||||
public double OffsetX { get; private set; } = offsetX;
|
|
||||||
public double OffsetY { get; private set; } = offsetY;
|
|
||||||
|
|
||||||
public static implicit operator Matrix(FrameworkMatrix m)
|
|
||||||
{
|
|
||||||
#if AVALONIA
|
#if AVALONIA
|
||||||
return new Matrix(m.M11, m.M12, m.M21, m.M22, m.M31, m.M32);
|
return new Matrix(m.M11, m.M12, m.M21, m.M22, m.M31, m.M32);
|
||||||
#else
|
#else
|
||||||
return new Matrix(m.M11, m.M12, m.M21, m.M22, m.OffsetX, m.OffsetY);
|
return new Matrix(m.M11, m.M12, m.M21, m.M22, m.OffsetX, m.OffsetY);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public static implicit operator FrameworkMatrix(Matrix m)
|
public static implicit operator FrameworkMatrix(Matrix m)
|
||||||
|
{
|
||||||
|
return new FrameworkMatrix(m.M11, m.M12, m.M21, m.M22, m.OffsetX, m.OffsetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly Point Transform(Point p)
|
||||||
|
{
|
||||||
|
return new Point(
|
||||||
|
M11 * p.X + M21 * p.Y + OffsetX,
|
||||||
|
M12 * p.X + M22 * p.Y + OffsetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Translate(double x, double y)
|
||||||
|
{
|
||||||
|
OffsetX += x;
|
||||||
|
OffsetY += y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Scale(double scaleX, double scaleY)
|
||||||
|
{
|
||||||
|
SetMatrix(
|
||||||
|
M11 * scaleX,
|
||||||
|
M12 * scaleY,
|
||||||
|
M21 * scaleX,
|
||||||
|
M22 * scaleY,
|
||||||
|
OffsetX * scaleX,
|
||||||
|
OffsetY * scaleY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Rotate(double angle)
|
||||||
|
{
|
||||||
|
angle = angle % 360d * Math.PI / 180d;
|
||||||
|
|
||||||
|
if (angle != 0d)
|
||||||
{
|
{
|
||||||
return new FrameworkMatrix(m.M11, m.M12, m.M21, m.M22, m.OffsetX, m.OffsetY);
|
var cos = Math.Cos(angle);
|
||||||
}
|
var sin = Math.Sin(angle);
|
||||||
|
|
||||||
public readonly Point Transform(Point p)
|
|
||||||
{
|
|
||||||
return new Point(
|
|
||||||
M11 * p.X + M21 * p.Y + OffsetX,
|
|
||||||
M12 * p.X + M22 * p.Y + OffsetY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Translate(double x, double y)
|
|
||||||
{
|
|
||||||
OffsetX += x;
|
|
||||||
OffsetY += y;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Scale(double scaleX, double scaleY)
|
|
||||||
{
|
|
||||||
SetMatrix(
|
|
||||||
M11 * scaleX,
|
|
||||||
M12 * scaleY,
|
|
||||||
M21 * scaleX,
|
|
||||||
M22 * scaleY,
|
|
||||||
OffsetX * scaleX,
|
|
||||||
OffsetY * scaleY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Rotate(double angle)
|
|
||||||
{
|
|
||||||
angle = angle % 360d * Math.PI / 180d;
|
|
||||||
|
|
||||||
if (angle != 0d)
|
|
||||||
{
|
|
||||||
var cos = Math.Cos(angle);
|
|
||||||
var sin = Math.Sin(angle);
|
|
||||||
|
|
||||||
SetMatrix(
|
|
||||||
M11 * cos - M12 * sin,
|
|
||||||
M11 * sin + M12 * cos,
|
|
||||||
M21 * cos - M22 * sin,
|
|
||||||
M21 * sin + M22 * cos,
|
|
||||||
OffsetX * cos - OffsetY * sin,
|
|
||||||
OffsetX * sin + OffsetY * cos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Invert()
|
|
||||||
{
|
|
||||||
var invDet = 1d / (M11 * M22 - M12 * M21);
|
|
||||||
|
|
||||||
if (double.IsInfinity(invDet))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Matrix is not invertible.");
|
|
||||||
}
|
|
||||||
|
|
||||||
SetMatrix(
|
SetMatrix(
|
||||||
invDet * M22, invDet * -M12, invDet * -M21, invDet * M11,
|
M11 * cos - M12 * sin,
|
||||||
invDet * (M21 * OffsetY - M22 * OffsetX),
|
M11 * sin + M12 * cos,
|
||||||
invDet * (M12 * OffsetX - M11 * OffsetY));
|
M21 * cos - M22 * sin,
|
||||||
}
|
M21 * sin + M22 * cos,
|
||||||
|
OffsetX * cos - OffsetY * sin,
|
||||||
public static Matrix Multiply(Matrix m1, Matrix m2)
|
OffsetX * sin + OffsetY * cos);
|
||||||
{
|
|
||||||
return new Matrix(
|
|
||||||
m1.M11 * m2.M11 + m1.M12 * m2.M21,
|
|
||||||
m1.M11 * m2.M12 + m1.M12 * m2.M22,
|
|
||||||
m1.M21 * m2.M11 + m1.M22 * m2.M21,
|
|
||||||
m1.M21 * m2.M12 + m1.M22 * m2.M22,
|
|
||||||
m1.OffsetX * m2.M11 + m1.OffsetY * m2.M21 + m2.OffsetX,
|
|
||||||
m1.OffsetX * m2.M12 + m1.OffsetY * m2.M22 + m2.OffsetY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetMatrix(double m11, double m12, double m21, double m22, double offsetX, double offsetY)
|
|
||||||
{
|
|
||||||
M11 = m11;
|
|
||||||
M12 = m12;
|
|
||||||
M21 = m21;
|
|
||||||
M22 = m22;
|
|
||||||
OffsetX = offsetX;
|
|
||||||
OffsetY = offsetY;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Invert()
|
||||||
|
{
|
||||||
|
var invDet = 1d / (M11 * M22 - M12 * M21);
|
||||||
|
|
||||||
|
if (double.IsInfinity(invDet))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Matrix is not invertible.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SetMatrix(
|
||||||
|
invDet * M22, invDet * -M12, invDet * -M21, invDet * M11,
|
||||||
|
invDet * (M21 * OffsetY - M22 * OffsetX),
|
||||||
|
invDet * (M12 * OffsetX - M11 * OffsetY));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Matrix Multiply(Matrix m1, Matrix m2)
|
||||||
|
{
|
||||||
|
return new Matrix(
|
||||||
|
m1.M11 * m2.M11 + m1.M12 * m2.M21,
|
||||||
|
m1.M11 * m2.M12 + m1.M12 * m2.M22,
|
||||||
|
m1.M21 * m2.M11 + m1.M22 * m2.M21,
|
||||||
|
m1.M21 * m2.M12 + m1.M22 * m2.M22,
|
||||||
|
m1.OffsetX * m2.M11 + m1.OffsetY * m2.M21 + m2.OffsetX,
|
||||||
|
m1.OffsetX * m2.M12 + m1.OffsetY * m2.M22 + m2.OffsetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMatrix(double m11, double m12, double m21, double m22, double offsetX, double offsetY)
|
||||||
|
{
|
||||||
|
M11 = m11;
|
||||||
|
M12 = m12;
|
||||||
|
M21 = m21;
|
||||||
|
M22 = m22;
|
||||||
|
OffsetX = offsetX;
|
||||||
|
OffsetY = offsetY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,53 +15,52 @@ using Avalonia.Layout;
|
||||||
using PathFigureCollection = Avalonia.Media.PathFigures;
|
using PathFigureCollection = Avalonia.Media.PathFigures;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws a metric grid overlay.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MetricGrid : MapGrid
|
||||||
{
|
{
|
||||||
/// <summary>
|
protected override void DrawGrid(PathFigureCollection figures, List<Label> labels)
|
||||||
/// Draws a metric grid overlay.
|
|
||||||
/// </summary>
|
|
||||||
public partial class MetricGrid : MapGrid
|
|
||||||
{
|
{
|
||||||
protected override void DrawGrid(PathFigureCollection figures, List<Label> labels)
|
var minLineDistance = Math.Max(MinLineDistance / ParentMap.ViewTransform.Scale, 1d);
|
||||||
|
var lineDistance = Math.Pow(10d, Math.Ceiling(Math.Log10(minLineDistance)));
|
||||||
|
|
||||||
|
if (lineDistance * 0.5 >= minLineDistance)
|
||||||
{
|
{
|
||||||
var minLineDistance = Math.Max(MinLineDistance / ParentMap.ViewTransform.Scale, 1d);
|
lineDistance *= 0.5;
|
||||||
var lineDistance = Math.Pow(10d, Math.Ceiling(Math.Log10(minLineDistance)));
|
|
||||||
|
|
||||||
if (lineDistance * 0.5 >= minLineDistance)
|
if (lineDistance * 0.4 >= minLineDistance)
|
||||||
{
|
{
|
||||||
lineDistance *= 0.5;
|
lineDistance *= 0.4;
|
||||||
|
|
||||||
if (lineDistance * 0.4 >= minLineDistance)
|
|
||||||
{
|
|
||||||
lineDistance *= 0.4;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var mapRect = ParentMap.ViewTransform.ViewToMapBounds(new Rect(0d, 0d, ParentMap.ActualWidth, ParentMap.ActualHeight));
|
var mapRect = ParentMap.ViewTransform.ViewToMapBounds(new Rect(0d, 0d, ParentMap.ActualWidth, ParentMap.ActualHeight));
|
||||||
var minX = Math.Ceiling(mapRect.X / lineDistance) * lineDistance;
|
var minX = Math.Ceiling(mapRect.X / lineDistance) * lineDistance;
|
||||||
var minY = Math.Ceiling(mapRect.Y / lineDistance) * lineDistance;
|
var minY = Math.Ceiling(mapRect.Y / lineDistance) * lineDistance;
|
||||||
|
|
||||||
for (var x = minX; x <= mapRect.X + mapRect.Width; x += lineDistance)
|
for (var x = minX; x <= mapRect.X + mapRect.Width; x += lineDistance)
|
||||||
{
|
{
|
||||||
var p1 = ParentMap.ViewTransform.MapToView(new Point(x, mapRect.Y));
|
var p1 = ParentMap.ViewTransform.MapToView(new Point(x, mapRect.Y));
|
||||||
var p2 = ParentMap.ViewTransform.MapToView(new Point(x, mapRect.Y + mapRect.Height));
|
var p2 = ParentMap.ViewTransform.MapToView(new Point(x, mapRect.Y + mapRect.Height));
|
||||||
figures.Add(CreateLineFigure(p1, p2));
|
figures.Add(CreateLineFigure(p1, p2));
|
||||||
|
|
||||||
var text = x.ToString("F0");
|
var text = x.ToString("F0");
|
||||||
labels.Add(new Label(text, p1.X, p1.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Bottom));
|
labels.Add(new Label(text, p1.X, p1.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Bottom));
|
||||||
labels.Add(new Label(text, p2.X, p2.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Top));
|
labels.Add(new Label(text, p2.X, p2.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Top));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var y = minY; y <= mapRect.Y + mapRect.Height; y += lineDistance)
|
for (var y = minY; y <= mapRect.Y + mapRect.Height; y += lineDistance)
|
||||||
{
|
{
|
||||||
var p1 = ParentMap.ViewTransform.MapToView(new Point(mapRect.X, y));
|
var p1 = ParentMap.ViewTransform.MapToView(new Point(mapRect.X, y));
|
||||||
var p2 = ParentMap.ViewTransform.MapToView(new Point(mapRect.X + mapRect.Width, y));
|
var p2 = ParentMap.ViewTransform.MapToView(new Point(mapRect.X + mapRect.Width, y));
|
||||||
figures.Add(CreateLineFigure(p1, p2));
|
figures.Add(CreateLineFigure(p1, p2));
|
||||||
|
|
||||||
var text = y.ToString("F0");
|
var text = y.ToString("F0");
|
||||||
labels.Add(new Label(text, p1.X, p1.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Bottom));
|
labels.Add(new Label(text, p1.X, p1.Y, 0d, HorizontalAlignment.Left, VerticalAlignment.Bottom));
|
||||||
labels.Add(new Label(text, p2.X, p2.Y, 0d, HorizontalAlignment.Right, VerticalAlignment.Bottom));
|
labels.Add(new Label(text, p2.X, p2.Y, 0d, HorizontalAlignment.Right, VerticalAlignment.Bottom));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,120 +6,119 @@ using System.Windows.Media;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Elliptical Polar Stereographic Projection with scale factor at the pole and
|
||||||
|
/// false easting and northing, as used by the UPS North and UPS South projections.
|
||||||
|
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.154-163.
|
||||||
|
/// </summary>
|
||||||
|
public class PolarStereographicProjection : MapProjection
|
||||||
{
|
{
|
||||||
/// <summary>
|
public override double GridConvergence(double latitude, double longitude)
|
||||||
/// Elliptical Polar Stereographic Projection with scale factor at the pole and
|
|
||||||
/// false easting and northing, as used by the UPS North and UPS South projections.
|
|
||||||
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.154-163.
|
|
||||||
/// </summary>
|
|
||||||
public class PolarStereographicProjection : MapProjection
|
|
||||||
{
|
{
|
||||||
public override double GridConvergence(double latitude, double longitude)
|
return Math.Sign(LatitudeOfOrigin) * (longitude - CentralMeridian);
|
||||||
{
|
|
||||||
return Math.Sign(LatitudeOfOrigin) * (longitude - CentralMeridian);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
|
||||||
{
|
|
||||||
var sign = Math.Sign(LatitudeOfOrigin);
|
|
||||||
var phi = sign * latitude * Math.PI / 180d;
|
|
||||||
|
|
||||||
var e = Math.Sqrt((2d - Flattening) * Flattening);
|
|
||||||
var eSinPhi = e * Math.Sin(phi);
|
|
||||||
var t = Math.Tan(Math.PI / 4d - phi / 2d)
|
|
||||||
/ Math.Pow((1d - eSinPhi) / (1d + eSinPhi), e / 2d); // p.161 (15-9)
|
|
||||||
// r == ρ/a
|
|
||||||
var r = 2d * ScaleFactor * t / Math.Sqrt(Math.Pow(1d + e, 1d + e) * Math.Pow(1d - e, 1d - e)); // p.161 (21-33)
|
|
||||||
var m = Math.Cos(phi) / Math.Sqrt(1d - eSinPhi * eSinPhi); // p.160 (14-15)
|
|
||||||
var k = r / m; // p.161 (21-32)
|
|
||||||
|
|
||||||
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
|
|
||||||
transform.Rotate(-sign * (longitude - CentralMeridian));
|
|
||||||
|
|
||||||
return transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Point LocationToMap(double latitude, double longitude)
|
|
||||||
{
|
|
||||||
var sign = Math.Sign(LatitudeOfOrigin);
|
|
||||||
var phi = sign * latitude * Math.PI / 180d;
|
|
||||||
var lambda = sign * longitude * Math.PI / 180d;
|
|
||||||
|
|
||||||
var e = Math.Sqrt((2d - Flattening) * Flattening);
|
|
||||||
var eSinPhi = e * Math.Sin(phi);
|
|
||||||
var t = Math.Tan(Math.PI / 4d - phi / 2d)
|
|
||||||
/ Math.Pow((1d - eSinPhi) / (1d + eSinPhi), e / 2d); // p.161 (15-9)
|
|
||||||
// ρ
|
|
||||||
var r = 2d * EquatorialRadius * ScaleFactor * t
|
|
||||||
/ Math.Sqrt(Math.Pow(1d + e, 1d + e) * Math.Pow(1d - e, 1d - e)); // p.161 (21-33)
|
|
||||||
|
|
||||||
var x = sign * r * Math.Sin(lambda); // p.161 (21-30)
|
|
||||||
var y = sign * -r * Math.Cos(lambda); // p.161 (21-31)
|
|
||||||
|
|
||||||
return new Point(x + FalseEasting, y + FalseNorthing);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Location MapToLocation(double x, double y)
|
|
||||||
{
|
|
||||||
var sign = Math.Sign(LatitudeOfOrigin);
|
|
||||||
x = sign * (x - FalseEasting);
|
|
||||||
y = sign * (y - FalseNorthing);
|
|
||||||
|
|
||||||
var e2 = (2d - Flattening) * Flattening;
|
|
||||||
var e = Math.Sqrt(e2);
|
|
||||||
var r = Math.Sqrt(x * x + y * y); // p.162 (20-18)
|
|
||||||
var t = r * Math.Sqrt(Math.Pow(1d + e, 1d + e) * Math.Pow(1d - e, 1d - e))
|
|
||||||
/ (2d * EquatorialRadius * ScaleFactor); // p.162 (21-39)
|
|
||||||
|
|
||||||
var phi = WorldMercatorProjection.ApproximateLatitude(e2, t); // p.162 (3-5)
|
|
||||||
var lambda = Math.Atan2(x, -y); // p.162 (20-16)
|
|
||||||
|
|
||||||
return new Location(sign * phi * 180d / Math.PI, sign * lambda * 180d / Math.PI);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||||
/// Universal Polar Stereographic North Projection - EPSG:32661.
|
|
||||||
/// </summary>
|
|
||||||
public class Wgs84UpsNorthProjection : PolarStereographicProjection
|
|
||||||
{
|
{
|
||||||
public const string DefaultCrsId = "EPSG:32661";
|
var sign = Math.Sign(LatitudeOfOrigin);
|
||||||
|
var phi = sign * latitude * Math.PI / 180d;
|
||||||
|
|
||||||
public Wgs84UpsNorthProjection() // parameterless constructor for XAML
|
var e = Math.Sqrt((2d - Flattening) * Flattening);
|
||||||
: this(DefaultCrsId)
|
var eSinPhi = e * Math.Sin(phi);
|
||||||
{
|
var t = Math.Tan(Math.PI / 4d - phi / 2d)
|
||||||
}
|
/ Math.Pow((1d - eSinPhi) / (1d + eSinPhi), e / 2d); // p.161 (15-9)
|
||||||
|
// r == ρ/a
|
||||||
|
var r = 2d * ScaleFactor * t / Math.Sqrt(Math.Pow(1d + e, 1d + e) * Math.Pow(1d - e, 1d - e)); // p.161 (21-33)
|
||||||
|
var m = Math.Cos(phi) / Math.Sqrt(1d - eSinPhi * eSinPhi); // p.160 (14-15)
|
||||||
|
var k = r / m; // p.161 (21-32)
|
||||||
|
|
||||||
public Wgs84UpsNorthProjection(string crsId)
|
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
|
||||||
{
|
transform.Rotate(-sign * (longitude - CentralMeridian));
|
||||||
CrsId = crsId;
|
|
||||||
ScaleFactor = 0.994;
|
return transform;
|
||||||
LatitudeOfOrigin = 90d;
|
|
||||||
FalseEasting = 2e6;
|
|
||||||
FalseNorthing = 2e6;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public override Point LocationToMap(double latitude, double longitude)
|
||||||
/// Universal Polar Stereographic South Projection - EPSG:32761.
|
|
||||||
/// </summary>
|
|
||||||
public class Wgs84UpsSouthProjection : PolarStereographicProjection
|
|
||||||
{
|
{
|
||||||
public const string DefaultCrsId = "EPSG:32761";
|
var sign = Math.Sign(LatitudeOfOrigin);
|
||||||
|
var phi = sign * latitude * Math.PI / 180d;
|
||||||
|
var lambda = sign * longitude * Math.PI / 180d;
|
||||||
|
|
||||||
public Wgs84UpsSouthProjection() // parameterless constructor for XAML
|
var e = Math.Sqrt((2d - Flattening) * Flattening);
|
||||||
: this(DefaultCrsId)
|
var eSinPhi = e * Math.Sin(phi);
|
||||||
{
|
var t = Math.Tan(Math.PI / 4d - phi / 2d)
|
||||||
}
|
/ Math.Pow((1d - eSinPhi) / (1d + eSinPhi), e / 2d); // p.161 (15-9)
|
||||||
|
// ρ
|
||||||
|
var r = 2d * EquatorialRadius * ScaleFactor * t
|
||||||
|
/ Math.Sqrt(Math.Pow(1d + e, 1d + e) * Math.Pow(1d - e, 1d - e)); // p.161 (21-33)
|
||||||
|
|
||||||
public Wgs84UpsSouthProjection(string crsId)
|
var x = sign * r * Math.Sin(lambda); // p.161 (21-30)
|
||||||
{
|
var y = sign * -r * Math.Cos(lambda); // p.161 (21-31)
|
||||||
CrsId = crsId;
|
|
||||||
ScaleFactor = 0.994;
|
return new Point(x + FalseEasting, y + FalseNorthing);
|
||||||
LatitudeOfOrigin = -90d;
|
}
|
||||||
FalseEasting = 2e6;
|
|
||||||
FalseNorthing = 2e6;
|
public override Location MapToLocation(double x, double y)
|
||||||
}
|
{
|
||||||
|
var sign = Math.Sign(LatitudeOfOrigin);
|
||||||
|
x = sign * (x - FalseEasting);
|
||||||
|
y = sign * (y - FalseNorthing);
|
||||||
|
|
||||||
|
var e2 = (2d - Flattening) * Flattening;
|
||||||
|
var e = Math.Sqrt(e2);
|
||||||
|
var r = Math.Sqrt(x * x + y * y); // p.162 (20-18)
|
||||||
|
var t = r * Math.Sqrt(Math.Pow(1d + e, 1d + e) * Math.Pow(1d - e, 1d - e))
|
||||||
|
/ (2d * EquatorialRadius * ScaleFactor); // p.162 (21-39)
|
||||||
|
|
||||||
|
var phi = WorldMercatorProjection.ApproximateLatitude(e2, t); // p.162 (3-5)
|
||||||
|
var lambda = Math.Atan2(x, -y); // p.162 (20-16)
|
||||||
|
|
||||||
|
return new Location(sign * phi * 180d / Math.PI, sign * lambda * 180d / Math.PI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Universal Polar Stereographic North Projection - EPSG:32661.
|
||||||
|
/// </summary>
|
||||||
|
public class Wgs84UpsNorthProjection : PolarStereographicProjection
|
||||||
|
{
|
||||||
|
public const string DefaultCrsId = "EPSG:32661";
|
||||||
|
|
||||||
|
public Wgs84UpsNorthProjection() // parameterless constructor for XAML
|
||||||
|
: this(DefaultCrsId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wgs84UpsNorthProjection(string crsId)
|
||||||
|
{
|
||||||
|
CrsId = crsId;
|
||||||
|
ScaleFactor = 0.994;
|
||||||
|
LatitudeOfOrigin = 90d;
|
||||||
|
FalseEasting = 2e6;
|
||||||
|
FalseNorthing = 2e6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Universal Polar Stereographic South Projection - EPSG:32761.
|
||||||
|
/// </summary>
|
||||||
|
public class Wgs84UpsSouthProjection : PolarStereographicProjection
|
||||||
|
{
|
||||||
|
public const string DefaultCrsId = "EPSG:32761";
|
||||||
|
|
||||||
|
public Wgs84UpsSouthProjection() // parameterless constructor for XAML
|
||||||
|
: this(DefaultCrsId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wgs84UpsSouthProjection(string crsId)
|
||||||
|
{
|
||||||
|
CrsId = crsId;
|
||||||
|
ScaleFactor = 0.994;
|
||||||
|
LatitudeOfOrigin = -90d;
|
||||||
|
FalseEasting = 2e6;
|
||||||
|
FalseNorthing = 2e6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,63 +3,62 @@ using System.Collections.ObjectModel;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An ObservableCollection of IEnumerable of Location. PolygonCollection adds a CollectionChanged
|
||||||
|
/// listener to each element that implements INotifyCollectionChanged and, when such an element changes,
|
||||||
|
/// fires its own CollectionChanged event with NotifyCollectionChangedAction.Replace for that element.
|
||||||
|
/// </summary>
|
||||||
|
public partial class PolygonCollection : ObservableCollection<IEnumerable<Location>>
|
||||||
{
|
{
|
||||||
/// <summary>
|
private void PolygonChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
/// An ObservableCollection of IEnumerable of Location. PolygonCollection adds a CollectionChanged
|
|
||||||
/// listener to each element that implements INotifyCollectionChanged and, when such an element changes,
|
|
||||||
/// fires its own CollectionChanged event with NotifyCollectionChangedAction.Replace for that element.
|
|
||||||
/// </summary>
|
|
||||||
public partial class PolygonCollection : ObservableCollection<IEnumerable<Location>>
|
|
||||||
{
|
{
|
||||||
private void PolygonChanged(object sender, NotifyCollectionChangedEventArgs e)
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InsertItem(int index, IEnumerable<Location> polygon)
|
||||||
|
{
|
||||||
|
if (polygon is INotifyCollectionChanged addedPolygon)
|
||||||
{
|
{
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender));
|
addedPolygon.CollectionChanged += PolygonChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void InsertItem(int index, IEnumerable<Location> polygon)
|
base.InsertItem(index, polygon);
|
||||||
{
|
}
|
||||||
if (polygon is INotifyCollectionChanged addedPolygon)
|
|
||||||
{
|
|
||||||
addedPolygon.CollectionChanged += PolygonChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.InsertItem(index, polygon);
|
protected override void SetItem(int index, IEnumerable<Location> polygon)
|
||||||
|
{
|
||||||
|
if (this[index] is INotifyCollectionChanged removedPolygon)
|
||||||
|
{
|
||||||
|
removedPolygon.CollectionChanged -= PolygonChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void SetItem(int index, IEnumerable<Location> polygon)
|
if (polygon is INotifyCollectionChanged addedPolygon)
|
||||||
{
|
{
|
||||||
if (this[index] is INotifyCollectionChanged removedPolygon)
|
addedPolygon.CollectionChanged += PolygonChanged;
|
||||||
{
|
|
||||||
removedPolygon.CollectionChanged -= PolygonChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (polygon is INotifyCollectionChanged addedPolygon)
|
|
||||||
{
|
|
||||||
addedPolygon.CollectionChanged += PolygonChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.SetItem(index, polygon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void RemoveItem(int index)
|
base.SetItem(index, polygon);
|
||||||
{
|
}
|
||||||
if (this[index] is INotifyCollectionChanged removedPolygon)
|
|
||||||
{
|
|
||||||
removedPolygon.CollectionChanged -= PolygonChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.RemoveItem(index);
|
protected override void RemoveItem(int index)
|
||||||
|
{
|
||||||
|
if (this[index] is INotifyCollectionChanged removedPolygon)
|
||||||
|
{
|
||||||
|
removedPolygon.CollectionChanged -= PolygonChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ClearItems()
|
base.RemoveItem(index);
|
||||||
{
|
}
|
||||||
foreach (var polygon in this.OfType<INotifyCollectionChanged>())
|
|
||||||
{
|
|
||||||
polygon.CollectionChanged -= PolygonChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.ClearItems();
|
protected override void ClearItems()
|
||||||
|
{
|
||||||
|
foreach (var polygon in this.OfType<INotifyCollectionChanged>())
|
||||||
|
{
|
||||||
|
polygon.CollectionChanged -= PolygonChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
base.ClearItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,106 +16,105 @@ using Avalonia.Layout;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class PushpinBorder
|
||||||
{
|
{
|
||||||
public partial class PushpinBorder
|
public Size ArrowSize
|
||||||
{
|
{
|
||||||
public Size ArrowSize
|
get => (Size)GetValue(ArrowSizeProperty);
|
||||||
{
|
set => SetValue(ArrowSizeProperty, value);
|
||||||
get => (Size)GetValue(ArrowSizeProperty);
|
|
||||||
set => SetValue(ArrowSizeProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double BorderWidth
|
|
||||||
{
|
|
||||||
get => (double)GetValue(BorderWidthProperty);
|
|
||||||
set => SetValue(BorderWidthProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual Geometry BuildGeometry()
|
|
||||||
{
|
|
||||||
var width = Math.Floor(ActualWidth);
|
|
||||||
var height = Math.Floor(ActualHeight);
|
|
||||||
var x1 = BorderWidth / 2d;
|
|
||||||
var y1 = BorderWidth / 2d;
|
|
||||||
var x2 = width - x1;
|
|
||||||
var y3 = height - y1;
|
|
||||||
var y2 = y3 - ArrowSize.Height;
|
|
||||||
var aw = ArrowSize.Width;
|
|
||||||
var r1 = CornerRadius.TopLeft;
|
|
||||||
var r2 = CornerRadius.TopRight;
|
|
||||||
var r3 = CornerRadius.BottomRight;
|
|
||||||
var r4 = CornerRadius.BottomLeft;
|
|
||||||
|
|
||||||
var figure = new PathFigure
|
|
||||||
{
|
|
||||||
StartPoint = new Point(x1, y1 + r1),
|
|
||||||
IsClosed = true,
|
|
||||||
IsFilled = true
|
|
||||||
};
|
|
||||||
|
|
||||||
figure.ArcTo(x1 + r1, y1, r1);
|
|
||||||
figure.LineTo(x2 - r2, y1);
|
|
||||||
figure.ArcTo(x2, y1 + r2, r2);
|
|
||||||
|
|
||||||
if (HorizontalAlignment == HorizontalAlignment.Right)
|
|
||||||
{
|
|
||||||
figure.LineTo(x2, y3);
|
|
||||||
figure.LineTo(x2 - aw, y2);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
figure.LineTo(x2, y2 - r3);
|
|
||||||
figure.ArcTo(x2 - r3, y2, r3);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (HorizontalAlignment == HorizontalAlignment.Center)
|
|
||||||
{
|
|
||||||
var c = width / 2d;
|
|
||||||
figure.LineTo(c + aw / 2d, y2);
|
|
||||||
figure.LineTo(c, y3);
|
|
||||||
figure.LineTo(c - aw / 2d, y2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (HorizontalAlignment == HorizontalAlignment.Left || HorizontalAlignment == HorizontalAlignment.Stretch)
|
|
||||||
{
|
|
||||||
figure.LineTo(x1 + aw, y2);
|
|
||||||
figure.LineTo(x1, y3);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
figure.LineTo(x1 + r4, y2);
|
|
||||||
figure.ArcTo(x1, y2 - r4, r4);
|
|
||||||
}
|
|
||||||
|
|
||||||
var geometry = new PathGeometry();
|
|
||||||
geometry.Figures.Add(figure);
|
|
||||||
|
|
||||||
return geometry;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class PathFigureExtensions
|
public double BorderWidth
|
||||||
{
|
{
|
||||||
public static void LineTo(this PathFigure figure, double x, double y)
|
get => (double)GetValue(BorderWidthProperty);
|
||||||
|
set => SetValue(BorderWidthProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual Geometry BuildGeometry()
|
||||||
|
{
|
||||||
|
var width = Math.Floor(ActualWidth);
|
||||||
|
var height = Math.Floor(ActualHeight);
|
||||||
|
var x1 = BorderWidth / 2d;
|
||||||
|
var y1 = BorderWidth / 2d;
|
||||||
|
var x2 = width - x1;
|
||||||
|
var y3 = height - y1;
|
||||||
|
var y2 = y3 - ArrowSize.Height;
|
||||||
|
var aw = ArrowSize.Width;
|
||||||
|
var r1 = CornerRadius.TopLeft;
|
||||||
|
var r2 = CornerRadius.TopRight;
|
||||||
|
var r3 = CornerRadius.BottomRight;
|
||||||
|
var r4 = CornerRadius.BottomLeft;
|
||||||
|
|
||||||
|
var figure = new PathFigure
|
||||||
{
|
{
|
||||||
figure.Segments.Add(new LineSegment
|
StartPoint = new Point(x1, y1 + r1),
|
||||||
{
|
IsClosed = true,
|
||||||
Point = new Point(x, y)
|
IsFilled = true
|
||||||
});
|
};
|
||||||
|
|
||||||
|
figure.ArcTo(x1 + r1, y1, r1);
|
||||||
|
figure.LineTo(x2 - r2, y1);
|
||||||
|
figure.ArcTo(x2, y1 + r2, r2);
|
||||||
|
|
||||||
|
if (HorizontalAlignment == HorizontalAlignment.Right)
|
||||||
|
{
|
||||||
|
figure.LineTo(x2, y3);
|
||||||
|
figure.LineTo(x2 - aw, y2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
figure.LineTo(x2, y2 - r3);
|
||||||
|
figure.ArcTo(x2 - r3, y2, r3);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ArcTo(this PathFigure figure, double x, double y, double r)
|
if (HorizontalAlignment == HorizontalAlignment.Center)
|
||||||
{
|
{
|
||||||
if (r > 0d)
|
var c = width / 2d;
|
||||||
|
figure.LineTo(c + aw / 2d, y2);
|
||||||
|
figure.LineTo(c, y3);
|
||||||
|
figure.LineTo(c - aw / 2d, y2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HorizontalAlignment == HorizontalAlignment.Left || HorizontalAlignment == HorizontalAlignment.Stretch)
|
||||||
|
{
|
||||||
|
figure.LineTo(x1 + aw, y2);
|
||||||
|
figure.LineTo(x1, y3);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
figure.LineTo(x1 + r4, y2);
|
||||||
|
figure.ArcTo(x1, y2 - r4, r4);
|
||||||
|
}
|
||||||
|
|
||||||
|
var geometry = new PathGeometry();
|
||||||
|
geometry.Figures.Add(figure);
|
||||||
|
|
||||||
|
return geometry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class PathFigureExtensions
|
||||||
|
{
|
||||||
|
public static void LineTo(this PathFigure figure, double x, double y)
|
||||||
|
{
|
||||||
|
figure.Segments.Add(new LineSegment
|
||||||
|
{
|
||||||
|
Point = new Point(x, y)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ArcTo(this PathFigure figure, double x, double y, double r)
|
||||||
|
{
|
||||||
|
if (r > 0d)
|
||||||
|
{
|
||||||
|
figure.Segments.Add(new ArcSegment
|
||||||
{
|
{
|
||||||
figure.Segments.Add(new ArcSegment
|
Point = new Point(x, y),
|
||||||
{
|
Size = new Size(r, r),
|
||||||
Point = new Point(x, y),
|
SweepDirection = SweepDirection.Clockwise
|
||||||
Size = new Size(r, r),
|
});
|
||||||
SweepDirection = SweepDirection.Clockwise
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,136 +7,135 @@ using System.Windows.Media;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spherical Stereographic Projection - AUTO2:97002.
|
||||||
|
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.157-160.
|
||||||
|
/// </summary>
|
||||||
|
public class StereographicProjection : MapProjection
|
||||||
{
|
{
|
||||||
/// <summary>
|
public const string DefaultCrsId = "AUTO2:97002"; // GeoServer non-standard CRS identifier
|
||||||
/// Spherical Stereographic Projection - AUTO2:97002.
|
|
||||||
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.157-160.
|
public StereographicProjection(string crsId)
|
||||||
/// </summary>
|
|
||||||
public class StereographicProjection : MapProjection
|
|
||||||
{
|
{
|
||||||
public const string DefaultCrsId = "AUTO2:97002"; // GeoServer non-standard CRS identifier
|
var parameters = crsId.Split(',');
|
||||||
|
|
||||||
public StereographicProjection(string crsId)
|
if (parameters.Length != 4 ||
|
||||||
|
string.IsNullOrEmpty(parameters[0]) ||
|
||||||
|
!double.TryParse(parameters[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double scaleFactor) ||
|
||||||
|
!double.TryParse(parameters[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double longitude) ||
|
||||||
|
!double.TryParse(parameters[3], NumberStyles.Float, CultureInfo.InvariantCulture, out double latitude))
|
||||||
{
|
{
|
||||||
var parameters = crsId.Split(',');
|
throw new ArgumentException($"Invalid CRS Identifier {crsId}.", nameof(crsId));
|
||||||
|
|
||||||
if (parameters.Length != 4 ||
|
|
||||||
string.IsNullOrEmpty(parameters[0]) ||
|
|
||||||
!double.TryParse(parameters[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double scaleFactor) ||
|
|
||||||
!double.TryParse(parameters[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double longitude) ||
|
|
||||||
!double.TryParse(parameters[3], NumberStyles.Float, CultureInfo.InvariantCulture, out double latitude))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Invalid CRS Identifier {crsId}.", nameof(crsId));
|
|
||||||
}
|
|
||||||
|
|
||||||
CrsId = crsId;
|
|
||||||
ScaleFactor = scaleFactor;
|
|
||||||
CentralMeridian = longitude;
|
|
||||||
LatitudeOfOrigin = latitude;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public StereographicProjection(double centerLatitude, double centerLongitude, double scaleFactor = 1d, string crsId = DefaultCrsId)
|
CrsId = crsId;
|
||||||
|
ScaleFactor = scaleFactor;
|
||||||
|
CentralMeridian = longitude;
|
||||||
|
LatitudeOfOrigin = latitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StereographicProjection(double centerLatitude, double centerLongitude, double scaleFactor = 1d, string crsId = DefaultCrsId)
|
||||||
|
{
|
||||||
|
CrsId = string.Format(CultureInfo.InvariantCulture,
|
||||||
|
"{0},{1:0.########},{2:0.########},{3:0.########}", crsId, scaleFactor, centerLongitude, centerLatitude);
|
||||||
|
ScaleFactor = scaleFactor;
|
||||||
|
CentralMeridian = centerLongitude;
|
||||||
|
LatitudeOfOrigin = centerLatitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetScaleAndGridConvergence(double latitude, double longitude, out double scale, out double gamma)
|
||||||
|
{
|
||||||
|
var phi0 = LatitudeOfOrigin * Math.PI / 180d; // φ1
|
||||||
|
var phi1 = latitude * Math.PI / 180d;
|
||||||
|
var phi2 = (latitude + 1e-3) * Math.PI / 180d;
|
||||||
|
var lambda = (longitude - CentralMeridian) * Math.PI / 180d; // λ - λ0
|
||||||
|
var sinPhi0 = Math.Sin(phi0);
|
||||||
|
var cosPhi0 = Math.Cos(phi0);
|
||||||
|
var sinPhi1 = Math.Sin(phi1);
|
||||||
|
var cosPhi1 = Math.Cos(phi1);
|
||||||
|
var sinPhi2 = Math.Sin(phi2);
|
||||||
|
var cosPhi2 = Math.Cos(phi2);
|
||||||
|
var sinLambda = Math.Sin(lambda);
|
||||||
|
var cosLambda = Math.Cos(lambda);
|
||||||
|
var k1 = 2d / (1d + sinPhi0 * sinPhi1 + cosPhi0 * cosPhi1 * cosLambda);
|
||||||
|
var k2 = 2d / (1d + sinPhi0 * sinPhi2 + cosPhi0 * cosPhi2 * cosLambda);
|
||||||
|
var c = k2 * cosPhi2 - k1 * cosPhi1;
|
||||||
|
var s = k2 * sinPhi2 - k1 * sinPhi1;
|
||||||
|
|
||||||
|
scale = k1;
|
||||||
|
gamma = Math.Atan2(-sinLambda * c, cosPhi0 * s - sinPhi0 * cosLambda * c) * 180d / Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double GridConvergence(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
GetScaleAndGridConvergence(latitude, longitude, out double _, out double gamma);
|
||||||
|
|
||||||
|
return gamma;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
GetScaleAndGridConvergence(latitude, longitude, out double scale, out double gamma);
|
||||||
|
|
||||||
|
var transform = new Matrix(scale, 0d, 0d, scale, 0d, 0d);
|
||||||
|
transform.Rotate(-gamma);
|
||||||
|
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Point LocationToMap(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
var phi0 = LatitudeOfOrigin * Math.PI / 180d; // φ1
|
||||||
|
var phi = latitude * Math.PI / 180d; // φ
|
||||||
|
var lambda = (longitude - CentralMeridian) * Math.PI / 180d; // λ - λ0
|
||||||
|
var sinPhi0 = Math.Sin(phi0);
|
||||||
|
var cosPhi0 = Math.Cos(phi0);
|
||||||
|
var sinPhi = Math.Sin(phi);
|
||||||
|
var cosPhi = Math.Cos(phi);
|
||||||
|
var sinLambda = Math.Sin(lambda);
|
||||||
|
var cosPhiCosLambda = cosPhi * Math.Cos(lambda);
|
||||||
|
var x = cosPhi * sinLambda;
|
||||||
|
var y = cosPhi0 * sinPhi - sinPhi0 * cosPhiCosLambda;
|
||||||
|
var k = 2d / (1d + sinPhi0 * sinPhi + cosPhi0 * cosPhiCosLambda); // p.157 (21-4), k0 == 1
|
||||||
|
|
||||||
|
return new Point(
|
||||||
|
EquatorialRadius * k * x,
|
||||||
|
EquatorialRadius * k * y); // p.157 (21-2/3)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Location MapToLocation(double x, double y)
|
||||||
|
{
|
||||||
|
var rho = Math.Sqrt(x * x + y * y);
|
||||||
|
var c = 2d * Math.Atan(rho / (2d * EquatorialRadius)); // p.159 (21-15), k0 == 1
|
||||||
|
var cosC = Math.Cos(c);
|
||||||
|
var sinC = Math.Sin(c);
|
||||||
|
|
||||||
|
var phi0 = LatitudeOfOrigin * Math.PI / 180d; // φ1
|
||||||
|
var cosPhi0 = Math.Cos(phi0);
|
||||||
|
var sinPhi0 = Math.Sin(phi0);
|
||||||
|
var phi = Math.Asin(cosC * sinPhi0 + y * sinC * cosPhi0 / rho); // (20-14)
|
||||||
|
double u, v;
|
||||||
|
|
||||||
|
if (LatitudeOfOrigin == 90d) // (20-16)
|
||||||
{
|
{
|
||||||
CrsId = string.Format(CultureInfo.InvariantCulture,
|
u = x;
|
||||||
"{0},{1:0.########},{2:0.########},{3:0.########}", crsId, scaleFactor, centerLongitude, centerLatitude);
|
v = -y;
|
||||||
ScaleFactor = scaleFactor;
|
}
|
||||||
CentralMeridian = centerLongitude;
|
else if (LatitudeOfOrigin == -90d) // (20-17)
|
||||||
LatitudeOfOrigin = centerLatitude;
|
{
|
||||||
|
u = x;
|
||||||
|
v = y;
|
||||||
|
}
|
||||||
|
else // (20-15)
|
||||||
|
{
|
||||||
|
u = x * sinC;
|
||||||
|
v = rho * cosPhi0 * cosC - y * sinPhi0 * sinC;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GetScaleAndGridConvergence(double latitude, double longitude, out double scale, out double gamma)
|
return new Location(
|
||||||
{
|
phi * 180d / Math.PI,
|
||||||
var phi0 = LatitudeOfOrigin * Math.PI / 180d; // φ1
|
Math.Atan2(u, v) * 180d / Math.PI + CentralMeridian);
|
||||||
var phi1 = latitude * Math.PI / 180d;
|
|
||||||
var phi2 = (latitude + 1e-3) * Math.PI / 180d;
|
|
||||||
var lambda = (longitude - CentralMeridian) * Math.PI / 180d; // λ - λ0
|
|
||||||
var sinPhi0 = Math.Sin(phi0);
|
|
||||||
var cosPhi0 = Math.Cos(phi0);
|
|
||||||
var sinPhi1 = Math.Sin(phi1);
|
|
||||||
var cosPhi1 = Math.Cos(phi1);
|
|
||||||
var sinPhi2 = Math.Sin(phi2);
|
|
||||||
var cosPhi2 = Math.Cos(phi2);
|
|
||||||
var sinLambda = Math.Sin(lambda);
|
|
||||||
var cosLambda = Math.Cos(lambda);
|
|
||||||
var k1 = 2d / (1d + sinPhi0 * sinPhi1 + cosPhi0 * cosPhi1 * cosLambda);
|
|
||||||
var k2 = 2d / (1d + sinPhi0 * sinPhi2 + cosPhi0 * cosPhi2 * cosLambda);
|
|
||||||
var c = k2 * cosPhi2 - k1 * cosPhi1;
|
|
||||||
var s = k2 * sinPhi2 - k1 * sinPhi1;
|
|
||||||
|
|
||||||
scale = k1;
|
|
||||||
gamma = Math.Atan2(-sinLambda * c, cosPhi0 * s - sinPhi0 * cosLambda * c) * 180d / Math.PI;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override double GridConvergence(double latitude, double longitude)
|
|
||||||
{
|
|
||||||
GetScaleAndGridConvergence(latitude, longitude, out double _, out double gamma);
|
|
||||||
|
|
||||||
return gamma;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
|
||||||
{
|
|
||||||
GetScaleAndGridConvergence(latitude, longitude, out double scale, out double gamma);
|
|
||||||
|
|
||||||
var transform = new Matrix(scale, 0d, 0d, scale, 0d, 0d);
|
|
||||||
transform.Rotate(-gamma);
|
|
||||||
|
|
||||||
return transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Point LocationToMap(double latitude, double longitude)
|
|
||||||
{
|
|
||||||
var phi0 = LatitudeOfOrigin * Math.PI / 180d; // φ1
|
|
||||||
var phi = latitude * Math.PI / 180d; // φ
|
|
||||||
var lambda = (longitude - CentralMeridian) * Math.PI / 180d; // λ - λ0
|
|
||||||
var sinPhi0 = Math.Sin(phi0);
|
|
||||||
var cosPhi0 = Math.Cos(phi0);
|
|
||||||
var sinPhi = Math.Sin(phi);
|
|
||||||
var cosPhi = Math.Cos(phi);
|
|
||||||
var sinLambda = Math.Sin(lambda);
|
|
||||||
var cosPhiCosLambda = cosPhi * Math.Cos(lambda);
|
|
||||||
var x = cosPhi * sinLambda;
|
|
||||||
var y = cosPhi0 * sinPhi - sinPhi0 * cosPhiCosLambda;
|
|
||||||
var k = 2d / (1d + sinPhi0 * sinPhi + cosPhi0 * cosPhiCosLambda); // p.157 (21-4), k0 == 1
|
|
||||||
|
|
||||||
return new Point(
|
|
||||||
EquatorialRadius * k * x,
|
|
||||||
EquatorialRadius * k * y); // p.157 (21-2/3)
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Location MapToLocation(double x, double y)
|
|
||||||
{
|
|
||||||
var rho = Math.Sqrt(x * x + y * y);
|
|
||||||
var c = 2d * Math.Atan(rho / (2d * EquatorialRadius)); // p.159 (21-15), k0 == 1
|
|
||||||
var cosC = Math.Cos(c);
|
|
||||||
var sinC = Math.Sin(c);
|
|
||||||
|
|
||||||
var phi0 = LatitudeOfOrigin * Math.PI / 180d; // φ1
|
|
||||||
var cosPhi0 = Math.Cos(phi0);
|
|
||||||
var sinPhi0 = Math.Sin(phi0);
|
|
||||||
var phi = Math.Asin(cosC * sinPhi0 + y * sinC * cosPhi0 / rho); // (20-14)
|
|
||||||
double u, v;
|
|
||||||
|
|
||||||
if (LatitudeOfOrigin == 90d) // (20-16)
|
|
||||||
{
|
|
||||||
u = x;
|
|
||||||
v = -y;
|
|
||||||
}
|
|
||||||
else if (LatitudeOfOrigin == -90d) // (20-17)
|
|
||||||
{
|
|
||||||
u = x;
|
|
||||||
v = y;
|
|
||||||
}
|
|
||||||
else // (20-15)
|
|
||||||
{
|
|
||||||
u = x * sinC;
|
|
||||||
v = rho * cosPhi0 * cosC - y * sinPhi0 * sinC;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Location(
|
|
||||||
phi * 180d / Math.PI,
|
|
||||||
Math.Atan2(u, v) * 180d / Math.PI + CentralMeridian);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,21 +10,20 @@ using Microsoft.UI.Xaml.Media;
|
||||||
using ImageSource = Avalonia.Media.IImage;
|
using ImageSource = Avalonia.Media.IImage;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public abstract class Tile(int zoomLevel, int x, int y, int columnCount)
|
||||||
{
|
{
|
||||||
public abstract class Tile(int zoomLevel, int x, int y, int columnCount)
|
public int ZoomLevel => zoomLevel;
|
||||||
{
|
public int X => x;
|
||||||
public int ZoomLevel => zoomLevel;
|
public int Y => y;
|
||||||
public int X => x;
|
public int Row => y;
|
||||||
public int Y => y;
|
public int Column { get; } = ((x % columnCount) + columnCount) % columnCount;
|
||||||
public int Row => y;
|
|
||||||
public int Column { get; } = ((x % columnCount) + columnCount) % columnCount;
|
|
||||||
|
|
||||||
public bool IsPending { get; set; } = true;
|
public bool IsPending { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs a tile image download Task and passes the result to the UI thread.
|
/// Runs a tile image download Task and passes the result to the UI thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc);
|
public abstract Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,225 +8,224 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads and optionally caches map tile images for a MapTilePyramidLayer.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITileImageLoader
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads and optionally caches map tile images for a MapTilePyramidLayer.
|
/// Starts asynchronous loading of all pending tiles from the tiles collection.
|
||||||
|
/// Caching is enabled when cacheName is a non-empty string and tiles are loaded from http or https Uris.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ITileImageLoader
|
void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress);
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Starts asynchronous loading of all pending tiles from the tiles collection.
|
|
||||||
/// Caching is enabled when cacheName is a non-empty string and tiles are loaded from http or https Uris.
|
|
||||||
/// </summary>
|
|
||||||
void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Terminates all running tile loading tasks.
|
/// Terminates all running tile loading tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void CancelLoadTiles();
|
void CancelLoadTiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TileImageLoader : ITileImageLoader
|
||||||
|
{
|
||||||
|
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(TileImageLoader));
|
||||||
|
|
||||||
|
private readonly Queue<Tile> tileQueue = new Queue<Tile>();
|
||||||
|
private int tileCount;
|
||||||
|
private int taskCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default folder path where a persistent cache implementation may save data, i.e. "C:\ProgramData\MapControl\TileCache".
|
||||||
|
/// </summary>
|
||||||
|
public static string DefaultCacheFolder =>
|
||||||
|
#if UWP
|
||||||
|
Path.Combine(Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path, "TileCache");
|
||||||
|
#else
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl", "TileCache");
|
||||||
|
#endif
|
||||||
|
/// <summary>
|
||||||
|
/// An IDistributedCache implementation used to cache tile images.
|
||||||
|
/// The default value is a MemoryDistributedCache instance.
|
||||||
|
/// </summary>
|
||||||
|
public static IDistributedCache Cache { get; set; } = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default expiration time for cached tile images. Used when no expiration time
|
||||||
|
/// was transmitted on download. The default value is one day.
|
||||||
|
/// </summary>
|
||||||
|
public static TimeSpan DefaultCacheExpiration { get; set; } = TimeSpan.FromDays(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum expiration time for cached tile images. A transmitted expiration time
|
||||||
|
/// that falls below this value is ignored. The default value is TimeSpan.Zero.
|
||||||
|
/// </summary>
|
||||||
|
public static TimeSpan MinCacheExpiration { get; set; } = TimeSpan.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum expiration time for cached tile images. A transmitted expiration time
|
||||||
|
/// that exceeds this value is ignored. The default value is ten days.
|
||||||
|
/// </summary>
|
||||||
|
public static TimeSpan MaxCacheExpiration { get; set; } = TimeSpan.FromDays(10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of parallel tile loading tasks. The default value is 4.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxLoadTasks { get; set; } = 4;
|
||||||
|
|
||||||
|
public void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress)
|
||||||
|
{
|
||||||
|
if (Cache == null)
|
||||||
|
{
|
||||||
|
cacheName = null; // disable caching
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (tileQueue)
|
||||||
|
{
|
||||||
|
tileQueue.Clear();
|
||||||
|
|
||||||
|
foreach (var tile in tiles.Where(tile => tile.IsPending))
|
||||||
|
{
|
||||||
|
tileQueue.Enqueue(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
tileCount = tileQueue.Count;
|
||||||
|
|
||||||
|
var maxTasks = Math.Min(tileCount, MaxLoadTasks);
|
||||||
|
|
||||||
|
while (taskCount < maxTasks)
|
||||||
|
{
|
||||||
|
taskCount++;
|
||||||
|
Logger?.LogDebug("Task count: {count}", taskCount);
|
||||||
|
|
||||||
|
_ = Task.Run(() => LoadTilesFromQueue(tileSource, cacheName, progress));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TileImageLoader : ITileImageLoader
|
public void CancelLoadTiles()
|
||||||
{
|
{
|
||||||
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(TileImageLoader));
|
lock (tileQueue)
|
||||||
|
|
||||||
private readonly Queue<Tile> tileQueue = new Queue<Tile>();
|
|
||||||
private int tileCount;
|
|
||||||
private int taskCount;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default folder path where a persistent cache implementation may save data, i.e. "C:\ProgramData\MapControl\TileCache".
|
|
||||||
/// </summary>
|
|
||||||
public static string DefaultCacheFolder =>
|
|
||||||
#if UWP
|
|
||||||
Path.Combine(Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path, "TileCache");
|
|
||||||
#else
|
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl", "TileCache");
|
|
||||||
#endif
|
|
||||||
/// <summary>
|
|
||||||
/// An IDistributedCache implementation used to cache tile images.
|
|
||||||
/// The default value is a MemoryDistributedCache instance.
|
|
||||||
/// </summary>
|
|
||||||
public static IDistributedCache Cache { get; set; } = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default expiration time for cached tile images. Used when no expiration time
|
|
||||||
/// was transmitted on download. The default value is one day.
|
|
||||||
/// </summary>
|
|
||||||
public static TimeSpan DefaultCacheExpiration { get; set; } = TimeSpan.FromDays(1);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Minimum expiration time for cached tile images. A transmitted expiration time
|
|
||||||
/// that falls below this value is ignored. The default value is TimeSpan.Zero.
|
|
||||||
/// </summary>
|
|
||||||
public static TimeSpan MinCacheExpiration { get; set; } = TimeSpan.Zero;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum expiration time for cached tile images. A transmitted expiration time
|
|
||||||
/// that exceeds this value is ignored. The default value is ten days.
|
|
||||||
/// </summary>
|
|
||||||
public static TimeSpan MaxCacheExpiration { get; set; } = TimeSpan.FromDays(10);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum number of parallel tile loading tasks. The default value is 4.
|
|
||||||
/// </summary>
|
|
||||||
public int MaxLoadTasks { get; set; } = 4;
|
|
||||||
|
|
||||||
public void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress)
|
|
||||||
{
|
{
|
||||||
if (Cache == null)
|
tileQueue.Clear();
|
||||||
{
|
tileCount = 0;
|
||||||
cacheName = null; // disable caching
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (tileQueue)
|
|
||||||
{
|
|
||||||
tileQueue.Clear();
|
|
||||||
|
|
||||||
foreach (var tile in tiles.Where(tile => tile.IsPending))
|
|
||||||
{
|
|
||||||
tileQueue.Enqueue(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
tileCount = tileQueue.Count;
|
|
||||||
|
|
||||||
var maxTasks = Math.Min(tileCount, MaxLoadTasks);
|
|
||||||
|
|
||||||
while (taskCount < maxTasks)
|
|
||||||
{
|
|
||||||
taskCount++;
|
|
||||||
Logger?.LogDebug("Task count: {count}", taskCount);
|
|
||||||
|
|
||||||
_ = Task.Run(() => LoadTilesFromQueue(tileSource, cacheName, progress));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void CancelLoadTiles()
|
private async Task LoadTilesFromQueue(TileSource tileSource, string cacheName, IProgress<double> progress)
|
||||||
|
{
|
||||||
|
bool TryDequeueTile(out Tile tile)
|
||||||
{
|
{
|
||||||
lock (tileQueue)
|
lock (tileQueue)
|
||||||
{
|
{
|
||||||
tileQueue.Clear();
|
if (tileQueue.Count > 0)
|
||||||
tileCount = 0;
|
{
|
||||||
|
tile = tileQueue.Dequeue();
|
||||||
|
tile.IsPending = false;
|
||||||
|
progress?.Report(1d - (double)tileQueue.Count / tileCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskCount--;
|
||||||
|
Logger?.LogDebug("Task count: {count}", taskCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tile = null;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadTilesFromQueue(TileSource tileSource, string cacheName, IProgress<double> progress)
|
while (TryDequeueTile(out Tile tile))
|
||||||
{
|
{
|
||||||
bool TryDequeueTile(out Tile tile)
|
|
||||||
{
|
|
||||||
lock (tileQueue)
|
|
||||||
{
|
|
||||||
if (tileQueue.Count > 0)
|
|
||||||
{
|
|
||||||
tile = tileQueue.Dequeue();
|
|
||||||
tile.IsPending = false;
|
|
||||||
progress?.Report(1d - (double)tileQueue.Count / tileCount);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
taskCount--;
|
|
||||||
Logger?.LogDebug("Task count: {count}", taskCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
tile = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (TryDequeueTile(out Tile tile))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Logger?.LogDebug("Thread {thread,2}: Loading tile ({zoom}/{column}/{row})",
|
|
||||||
Environment.CurrentManagedThreadId, tile.ZoomLevel, tile.Column, tile.Row);
|
|
||||||
|
|
||||||
await LoadTileImage(tile, tileSource, cacheName);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger?.LogError(ex, "Failed loading tile {zoom}/{column}/{row}", tile.ZoomLevel, tile.Column, tile.Row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task LoadTileImage(Tile tile, TileSource tileSource, string cacheName)
|
|
||||||
{
|
|
||||||
// Pass image loading callbacks to platform-specific method
|
|
||||||
// tile.LoadImageAsync(Func<Task<ImageSource>>) for completion in the UI thread.
|
|
||||||
|
|
||||||
var uri = tileSource.GetUri(tile.ZoomLevel, tile.Column, tile.Row);
|
|
||||||
|
|
||||||
if (uri == null)
|
|
||||||
{
|
|
||||||
await tile.LoadImageAsync(() => tileSource.LoadImageAsync(tile.ZoomLevel, tile.Column, tile.Row)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else if (!uri.IsHttp() || string.IsNullOrEmpty(cacheName))
|
|
||||||
{
|
|
||||||
await tile.LoadImageAsync(() => tileSource.LoadImageAsync(uri)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var buffer = await LoadCachedBuffer(tile, uri, cacheName).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (buffer != null)
|
|
||||||
{
|
|
||||||
await tile.LoadImageAsync(() => tileSource.LoadImageAsync(buffer)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<byte[]> LoadCachedBuffer(Tile tile, Uri uri, string cacheName)
|
|
||||||
{
|
|
||||||
var extension = Path.GetExtension(uri.LocalPath).ToLower();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(extension) || extension == ".jpeg")
|
|
||||||
{
|
|
||||||
extension = ".jpg";
|
|
||||||
}
|
|
||||||
|
|
||||||
var cacheKey = $"{cacheName}/{tile.ZoomLevel}/{tile.Column}/{tile.Row}{extension}";
|
|
||||||
byte[] buffer = null;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
buffer = await Cache.GetAsync(cacheKey).ConfigureAwait(false);
|
Logger?.LogDebug("Thread {thread,2}: Loading tile ({zoom}/{column}/{row})",
|
||||||
|
Environment.CurrentManagedThreadId, tile.ZoomLevel, tile.Column, tile.Row);
|
||||||
|
|
||||||
|
await LoadTileImage(tile, tileSource, cacheName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger?.LogError(ex, "Cache.GetAsync({cacheKey})", cacheKey);
|
Logger?.LogError(ex, "Failed loading tile {zoom}/{column}/{row}", tile.ZoomLevel, tile.Column, tile.Row);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer == null)
|
|
||||||
{
|
|
||||||
using var response = await ImageLoader.GetHttpResponseAsync(uri).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (response != null)
|
|
||||||
{
|
|
||||||
buffer = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var maxAge = response.Headers.CacheControl?.MaxAge;
|
|
||||||
var options = new DistributedCacheEntryOptions
|
|
||||||
{
|
|
||||||
AbsoluteExpirationRelativeToNow =
|
|
||||||
!maxAge.HasValue ? DefaultCacheExpiration
|
|
||||||
: maxAge.Value < MinCacheExpiration ? MinCacheExpiration
|
|
||||||
: maxAge.Value > MaxCacheExpiration ? MaxCacheExpiration
|
|
||||||
: maxAge.Value
|
|
||||||
};
|
|
||||||
|
|
||||||
await Cache.SetAsync(cacheKey, buffer, options).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger?.LogError(ex, "Cache.SetAsync({cacheKey})", cacheKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task LoadTileImage(Tile tile, TileSource tileSource, string cacheName)
|
||||||
|
{
|
||||||
|
// Pass image loading callbacks to platform-specific method
|
||||||
|
// tile.LoadImageAsync(Func<Task<ImageSource>>) for completion in the UI thread.
|
||||||
|
|
||||||
|
var uri = tileSource.GetUri(tile.ZoomLevel, tile.Column, tile.Row);
|
||||||
|
|
||||||
|
if (uri == null)
|
||||||
|
{
|
||||||
|
await tile.LoadImageAsync(() => tileSource.LoadImageAsync(tile.ZoomLevel, tile.Column, tile.Row)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else if (!uri.IsHttp() || string.IsNullOrEmpty(cacheName))
|
||||||
|
{
|
||||||
|
await tile.LoadImageAsync(() => tileSource.LoadImageAsync(uri)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var buffer = await LoadCachedBuffer(tile, uri, cacheName).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (buffer != null)
|
||||||
|
{
|
||||||
|
await tile.LoadImageAsync(() => tileSource.LoadImageAsync(buffer)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<byte[]> LoadCachedBuffer(Tile tile, Uri uri, string cacheName)
|
||||||
|
{
|
||||||
|
var extension = Path.GetExtension(uri.LocalPath).ToLower();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(extension) || extension == ".jpeg")
|
||||||
|
{
|
||||||
|
extension = ".jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheKey = $"{cacheName}/{tile.ZoomLevel}/{tile.Column}/{tile.Row}{extension}";
|
||||||
|
byte[] buffer = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
buffer = await Cache.GetAsync(cacheKey).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger?.LogError(ex, "Cache.GetAsync({cacheKey})", cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer == null)
|
||||||
|
{
|
||||||
|
using var response = await ImageLoader.GetHttpResponseAsync(uri).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (response != null)
|
||||||
|
{
|
||||||
|
buffer = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var maxAge = response.Headers.CacheControl?.MaxAge;
|
||||||
|
var options = new DistributedCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow =
|
||||||
|
!maxAge.HasValue ? DefaultCacheExpiration
|
||||||
|
: maxAge.Value < MinCacheExpiration ? MinCacheExpiration
|
||||||
|
: maxAge.Value > MaxCacheExpiration ? MaxCacheExpiration
|
||||||
|
: maxAge.Value
|
||||||
|
};
|
||||||
|
|
||||||
|
await Cache.SetAsync(cacheKey, buffer, options).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger?.LogError(ex, "Cache.SetAsync({cacheKey})", cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class TileMatrix(int zoomLevel, int xMin, int yMin, int xMax, int yMax)
|
||||||
{
|
{
|
||||||
public class TileMatrix(int zoomLevel, int xMin, int yMin, int xMax, int yMax)
|
public int ZoomLevel => zoomLevel;
|
||||||
{
|
public int XMin => xMin;
|
||||||
public int ZoomLevel => zoomLevel;
|
public int YMin => yMin;
|
||||||
public int XMin => xMin;
|
public int XMax => xMax;
|
||||||
public int YMin => yMin;
|
public int YMax => yMax;
|
||||||
public int XMax => xMax;
|
public int Width => xMax - xMin + 1;
|
||||||
public int YMax => yMax;
|
public int Height => yMax - yMin + 1;
|
||||||
public int Width => xMax - xMin + 1;
|
|
||||||
public int Height => yMax - yMin + 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,188 +21,187 @@ using Avalonia.Controls;
|
||||||
using Brush = Avalonia.Media.IBrush;
|
using Brush = Avalonia.Media.IBrush;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public abstract class TilePyramidLayer : Panel, IMapLayer
|
||||||
{
|
{
|
||||||
public abstract class TilePyramidLayer : Panel, IMapLayer
|
public static readonly DependencyProperty SourceNameProperty =
|
||||||
|
DependencyPropertyHelper.Register<TilePyramidLayer, string>(nameof(SourceName));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty DescriptionProperty =
|
||||||
|
DependencyPropertyHelper.Register<TilePyramidLayer, string>(nameof(Description));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MaxBackgroundLevelsProperty =
|
||||||
|
DependencyPropertyHelper.Register<TilePyramidLayer, int>(nameof(MaxBackgroundLevels), 5);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty UpdateIntervalProperty =
|
||||||
|
DependencyPropertyHelper.Register<TilePyramidLayer, TimeSpan>(nameof(UpdateInterval), TimeSpan.FromSeconds(0.2),
|
||||||
|
(layer, oldValue, newValue) => layer.updateTimer.Interval = newValue);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty UpdateWhileViewportChangingProperty =
|
||||||
|
DependencyPropertyHelper.Register<TilePyramidLayer, bool>(nameof(UpdateWhileViewportChanging));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MapBackgroundProperty =
|
||||||
|
DependencyPropertyHelper.Register<TilePyramidLayer, Brush>(nameof(MapBackground));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MapForegroundProperty =
|
||||||
|
DependencyPropertyHelper.Register<TilePyramidLayer, Brush>(nameof(MapForeground));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty LoadingProgressProperty =
|
||||||
|
DependencyPropertyHelper.Register<TilePyramidLayer, double>(nameof(LoadingProgress), 1d);
|
||||||
|
|
||||||
|
private readonly Progress<double> loadingProgress;
|
||||||
|
private readonly UpdateTimer updateTimer;
|
||||||
|
|
||||||
|
protected TilePyramidLayer()
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty SourceNameProperty =
|
IsHitTestVisible = false;
|
||||||
DependencyPropertyHelper.Register<TilePyramidLayer, string>(nameof(SourceName));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty DescriptionProperty =
|
loadingProgress = new Progress<double>(p => SetValue(LoadingProgressProperty, p));
|
||||||
DependencyPropertyHelper.Register<TilePyramidLayer, string>(nameof(Description));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MaxBackgroundLevelsProperty =
|
updateTimer = new UpdateTimer { Interval = UpdateInterval };
|
||||||
DependencyPropertyHelper.Register<TilePyramidLayer, int>(nameof(MaxBackgroundLevels), 5);
|
updateTimer.Tick += (_, _) => UpdateTiles();
|
||||||
|
|
||||||
public static readonly DependencyProperty UpdateIntervalProperty =
|
|
||||||
DependencyPropertyHelper.Register<TilePyramidLayer, TimeSpan>(nameof(UpdateInterval), TimeSpan.FromSeconds(0.2),
|
|
||||||
(layer, oldValue, newValue) => layer.updateTimer.Interval = newValue);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty UpdateWhileViewportChangingProperty =
|
|
||||||
DependencyPropertyHelper.Register<TilePyramidLayer, bool>(nameof(UpdateWhileViewportChanging));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MapBackgroundProperty =
|
|
||||||
DependencyPropertyHelper.Register<TilePyramidLayer, Brush>(nameof(MapBackground));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MapForegroundProperty =
|
|
||||||
DependencyPropertyHelper.Register<TilePyramidLayer, Brush>(nameof(MapForeground));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty LoadingProgressProperty =
|
|
||||||
DependencyPropertyHelper.Register<TilePyramidLayer, double>(nameof(LoadingProgress), 1d);
|
|
||||||
|
|
||||||
private readonly Progress<double> loadingProgress;
|
|
||||||
private readonly UpdateTimer updateTimer;
|
|
||||||
|
|
||||||
protected TilePyramidLayer()
|
|
||||||
{
|
|
||||||
IsHitTestVisible = false;
|
|
||||||
|
|
||||||
loadingProgress = new Progress<double>(p => SetValue(LoadingProgressProperty, p));
|
|
||||||
|
|
||||||
updateTimer = new UpdateTimer { Interval = UpdateInterval };
|
|
||||||
updateTimer.Tick += (_, _) => UpdateTiles();
|
|
||||||
#if WPF
|
#if WPF
|
||||||
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
|
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
|
||||||
#elif UWP || WINUI
|
#elif UWP || WINUI
|
||||||
ElementCompositionPreview.GetElementVisual(this).BorderMode = CompositionBorderMode.Hard;
|
ElementCompositionPreview.GetElementVisual(this).BorderMode = CompositionBorderMode.Hard;
|
||||||
MapPanel.InitMapElement(this);
|
MapPanel.InitMapElement(this);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public ITileImageLoader TileImageLoader
|
public ITileImageLoader TileImageLoader
|
||||||
|
{
|
||||||
|
get => field ??= new TileImageLoader();
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the tile source that is used as component of a tile cache key.
|
||||||
|
/// Tile images are not cached when SourceName is null or empty.
|
||||||
|
/// </summary>
|
||||||
|
public string SourceName
|
||||||
|
{
|
||||||
|
get => (string)GetValue(SourceNameProperty);
|
||||||
|
set => SetValue(SourceNameProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Description of the layer. Used to display copyright information on top of the map.
|
||||||
|
/// </summary>
|
||||||
|
public string Description
|
||||||
|
{
|
||||||
|
get => (string)GetValue(DescriptionProperty);
|
||||||
|
set => SetValue(DescriptionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of background tile levels. Default value is 5.
|
||||||
|
/// Only effective in a MapTileLayer or WmtsTileLayer that is the MapLayer of its ParentMap.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxBackgroundLevels
|
||||||
|
{
|
||||||
|
get => (int)GetValue(MaxBackgroundLevelsProperty);
|
||||||
|
set => SetValue(MaxBackgroundLevelsProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum time interval between tile updates.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan UpdateInterval
|
||||||
|
{
|
||||||
|
get => (TimeSpan)GetValue(UpdateIntervalProperty);
|
||||||
|
set => SetValue(UpdateIntervalProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls if tiles are updated while the viewport is still changing.
|
||||||
|
/// </summary>
|
||||||
|
public bool UpdateWhileViewportChanging
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(UpdateWhileViewportChangingProperty);
|
||||||
|
set => SetValue(UpdateWhileViewportChangingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional background brush. Sets MapBase.Background if not null and this layer is the base map layer.
|
||||||
|
/// </summary>
|
||||||
|
public Brush MapBackground
|
||||||
|
{
|
||||||
|
get => (Brush)GetValue(MapBackgroundProperty);
|
||||||
|
set => SetValue(MapBackgroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional foreground brush. Sets MapBase.Foreground if not null and this layer is the base map layer.
|
||||||
|
/// </summary>
|
||||||
|
public Brush MapForeground
|
||||||
|
{
|
||||||
|
get => (Brush)GetValue(MapForegroundProperty);
|
||||||
|
set => SetValue(MapForegroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the progress of the TileImageLoader as a double value between 0 and 1.
|
||||||
|
/// </summary>
|
||||||
|
public double LoadingProgress => (double)GetValue(LoadingProgressProperty);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements IMapElement.ParentMap.
|
||||||
|
/// </summary>
|
||||||
|
public MapBase ParentMap
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set
|
||||||
{
|
{
|
||||||
get => field ??= new TileImageLoader();
|
if (field != null)
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Name of the tile source that is used as component of a tile cache key.
|
|
||||||
/// Tile images are not cached when SourceName is null or empty.
|
|
||||||
/// </summary>
|
|
||||||
public string SourceName
|
|
||||||
{
|
|
||||||
get => (string)GetValue(SourceNameProperty);
|
|
||||||
set => SetValue(SourceNameProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Description of the layer. Used to display copyright information on top of the map.
|
|
||||||
/// </summary>
|
|
||||||
public string Description
|
|
||||||
{
|
|
||||||
get => (string)GetValue(DescriptionProperty);
|
|
||||||
set => SetValue(DescriptionProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum number of background tile levels. Default value is 5.
|
|
||||||
/// Only effective in a MapTileLayer or WmtsTileLayer that is the MapLayer of its ParentMap.
|
|
||||||
/// </summary>
|
|
||||||
public int MaxBackgroundLevels
|
|
||||||
{
|
|
||||||
get => (int)GetValue(MaxBackgroundLevelsProperty);
|
|
||||||
set => SetValue(MaxBackgroundLevelsProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Minimum time interval between tile updates.
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan UpdateInterval
|
|
||||||
{
|
|
||||||
get => (TimeSpan)GetValue(UpdateIntervalProperty);
|
|
||||||
set => SetValue(UpdateIntervalProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls if tiles are updated while the viewport is still changing.
|
|
||||||
/// </summary>
|
|
||||||
public bool UpdateWhileViewportChanging
|
|
||||||
{
|
|
||||||
get => (bool)GetValue(UpdateWhileViewportChangingProperty);
|
|
||||||
set => SetValue(UpdateWhileViewportChangingProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional background brush. Sets MapBase.Background if not null and this layer is the base map layer.
|
|
||||||
/// </summary>
|
|
||||||
public Brush MapBackground
|
|
||||||
{
|
|
||||||
get => (Brush)GetValue(MapBackgroundProperty);
|
|
||||||
set => SetValue(MapBackgroundProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional foreground brush. Sets MapBase.Foreground if not null and this layer is the base map layer.
|
|
||||||
/// </summary>
|
|
||||||
public Brush MapForeground
|
|
||||||
{
|
|
||||||
get => (Brush)GetValue(MapForegroundProperty);
|
|
||||||
set => SetValue(MapForegroundProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the progress of the TileImageLoader as a double value between 0 and 1.
|
|
||||||
/// </summary>
|
|
||||||
public double LoadingProgress => (double)GetValue(LoadingProgressProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implements IMapElement.ParentMap.
|
|
||||||
/// </summary>
|
|
||||||
public MapBase ParentMap
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set
|
|
||||||
{
|
{
|
||||||
if (field != null)
|
field.ViewportChanged -= OnViewportChanged;
|
||||||
{
|
|
||||||
field.ViewportChanged -= OnViewportChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
field = value;
|
|
||||||
|
|
||||||
if (field != null)
|
|
||||||
{
|
|
||||||
field.ViewportChanged += OnViewportChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTimer.Run();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsBaseMapLayer => ParentMap != null && ParentMap.Children.Count > 0 && ParentMap.Children[0] == this;
|
field = value;
|
||||||
|
|
||||||
protected void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName)
|
if (field != null)
|
||||||
{
|
|
||||||
TileImageLoader.BeginLoadTiles(tiles, tileSource, cacheName, loadingProgress);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void CancelLoadTiles()
|
|
||||||
{
|
|
||||||
TileImageLoader.CancelLoadTiles();
|
|
||||||
ClearValue(LoadingProgressProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void UpdateRenderTransform();
|
|
||||||
|
|
||||||
protected abstract void UpdateTileCollection();
|
|
||||||
|
|
||||||
private void UpdateTiles()
|
|
||||||
{
|
|
||||||
updateTimer.Stop();
|
|
||||||
UpdateTileCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.TransformCenterChanged || e.ProjectionChanged || Children.Count == 0)
|
|
||||||
{
|
{
|
||||||
UpdateTiles(); // update immediately
|
field.ViewportChanged += OnViewportChanged;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UpdateRenderTransform();
|
|
||||||
updateTimer.Run(!UpdateWhileViewportChanging);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTimer.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsBaseMapLayer => ParentMap != null && ParentMap.Children.Count > 0 && ParentMap.Children[0] == this;
|
||||||
|
|
||||||
|
protected void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName)
|
||||||
|
{
|
||||||
|
TileImageLoader.BeginLoadTiles(tiles, tileSource, cacheName, loadingProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CancelLoadTiles()
|
||||||
|
{
|
||||||
|
TileImageLoader.CancelLoadTiles();
|
||||||
|
ClearValue(LoadingProgressProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void UpdateRenderTransform();
|
||||||
|
|
||||||
|
protected abstract void UpdateTileCollection();
|
||||||
|
|
||||||
|
private void UpdateTiles()
|
||||||
|
{
|
||||||
|
updateTimer.Stop();
|
||||||
|
UpdateTileCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.TransformCenterChanged || e.ProjectionChanged || Children.Count == 0)
|
||||||
|
{
|
||||||
|
UpdateTiles(); // update immediately
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateRenderTransform();
|
||||||
|
updateTimer.Run(!UpdateWhileViewportChanging);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,60 +10,59 @@ using Microsoft.UI.Xaml.Media;
|
||||||
using ImageSource = Avalonia.Media.IImage;
|
using ImageSource = Avalonia.Media.IImage;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides the download Uri or ImageSource of map tiles. Used by TileImageLoader.
|
||||||
|
/// </summary>
|
||||||
|
#if UWP || WINUI
|
||||||
|
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||||
|
#else
|
||||||
|
[System.ComponentModel.TypeConverter(typeof(TileSourceConverter))]
|
||||||
|
#endif
|
||||||
|
public class TileSource
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides the download Uri or ImageSource of map tiles. Used by TileImageLoader.
|
/// Gets an image request Uri for the specified zoom level and tile indices.
|
||||||
|
/// May return null when the image shall be loaded by
|
||||||
|
/// the LoadImageAsync(zoomLevel, column, row) method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#if UWP || WINUI
|
public virtual Uri GetUri(int zoomLevel, int column, int row)
|
||||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
|
||||||
#else
|
|
||||||
[System.ComponentModel.TypeConverter(typeof(TileSourceConverter))]
|
|
||||||
#endif
|
|
||||||
public class TileSource
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
return null;
|
||||||
/// Gets an image request Uri for the specified zoom level and tile indices.
|
}
|
||||||
/// May return null when the image shall be loaded by
|
|
||||||
/// the LoadImageAsync(zoomLevel, column, row) method.
|
|
||||||
/// </summary>
|
|
||||||
public virtual Uri GetUri(int zoomLevel, int column, int row)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a tile image without an Uri. Called when GetUri returns null.
|
/// Loads a tile image without an Uri. Called when GetUri returns null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual Task<ImageSource> LoadImageAsync(int zoomLevel, int column, int row)
|
public virtual Task<ImageSource> LoadImageAsync(int zoomLevel, int column, int row)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a tile image from an Uri. Called when the Uri scheme is neither
|
/// Loads a tile image from an Uri. Called when the Uri scheme is neither
|
||||||
/// http nor https or when the TileImageLoader is not using an image cache.
|
/// http nor https or when the TileImageLoader is not using an image cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual Task<ImageSource> LoadImageAsync(Uri uri)
|
public virtual Task<ImageSource> LoadImageAsync(Uri uri)
|
||||||
{
|
{
|
||||||
return ImageLoader.LoadImageAsync(uri);
|
return ImageLoader.LoadImageAsync(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a tile image from an encoded image buffer. Called when the
|
/// Loads a tile image from an encoded image buffer. Called when the
|
||||||
/// TileImageLoader caches image buffers from http or https requests.
|
/// TileImageLoader caches image buffers from http or https requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual Task<ImageSource> LoadImageAsync(byte[] buffer)
|
public virtual Task<ImageSource> LoadImageAsync(byte[] buffer)
|
||||||
{
|
{
|
||||||
return ImageLoader.LoadImageAsync(buffer);
|
return ImageLoader.LoadImageAsync(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a TileSource instance from an Uri template string.
|
/// Creates a TileSource instance from an Uri template string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static TileSource Parse(string uriTemplate)
|
public static TileSource Parse(string uriTemplate)
|
||||||
{
|
{
|
||||||
return new UriTileSource { UriTemplate = uriTemplate };
|
return new UriTileSource { UriTemplate = uriTemplate };
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,165 +6,164 @@ using System.Windows.Media;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transverse Mercator Projection. See
|
||||||
|
/// https://en.wikipedia.org/wiki/Transverse_Mercator_projection,
|
||||||
|
/// https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system,
|
||||||
|
/// https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Convergence.
|
||||||
|
/// </summary>
|
||||||
|
public class TransverseMercatorProjection : MapProjection
|
||||||
{
|
{
|
||||||
/// <summary>
|
private readonly double n;
|
||||||
/// Transverse Mercator Projection. See
|
private readonly double m; // 2*sqrt(n)/(1+n)
|
||||||
/// https://en.wikipedia.org/wiki/Transverse_Mercator_projection,
|
private readonly double a1; // α1
|
||||||
/// https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system,
|
private readonly double a2; // α2
|
||||||
/// https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Convergence.
|
private readonly double a3; // α3
|
||||||
/// </summary>
|
private readonly double b1; // β1
|
||||||
public class TransverseMercatorProjection : MapProjection
|
private readonly double b2; // β2
|
||||||
|
private readonly double b3; // β3
|
||||||
|
private readonly double d1; // δ1
|
||||||
|
private readonly double d2; // δ2
|
||||||
|
private readonly double d3; // δ3
|
||||||
|
private readonly double A;
|
||||||
|
|
||||||
|
protected TransverseMercatorProjection(double equatorialRadius, double flattening)
|
||||||
{
|
{
|
||||||
private readonly double n;
|
EquatorialRadius = equatorialRadius;
|
||||||
private readonly double m; // 2*sqrt(n)/(1+n)
|
Flattening = flattening;
|
||||||
private readonly double a1; // α1
|
|
||||||
private readonly double a2; // α2
|
|
||||||
private readonly double a3; // α3
|
|
||||||
private readonly double b1; // β1
|
|
||||||
private readonly double b2; // β2
|
|
||||||
private readonly double b3; // β3
|
|
||||||
private readonly double d1; // δ1
|
|
||||||
private readonly double d2; // δ2
|
|
||||||
private readonly double d3; // δ3
|
|
||||||
private readonly double A;
|
|
||||||
|
|
||||||
protected TransverseMercatorProjection(double equatorialRadius, double flattening)
|
n = flattening / (2d - flattening);
|
||||||
{
|
m = 2d * Math.Sqrt(n) / (1d + n);
|
||||||
EquatorialRadius = equatorialRadius;
|
var n2 = n * n;
|
||||||
Flattening = flattening;
|
var n3 = n * n2;
|
||||||
|
|
||||||
n = flattening / (2d - flattening);
|
a1 = n / 2d - n2 * 2d / 3d + n3 * 5d / 16d;
|
||||||
m = 2d * Math.Sqrt(n) / (1d + n);
|
a2 = n2 * 13d / 48d - n3 * 3d / 5d;
|
||||||
var n2 = n * n;
|
a3 = n3 * 61d / 240d;
|
||||||
var n3 = n * n2;
|
b1 = n / 2d - n2 * 2d / 3d + n3 * 37d / 96d;
|
||||||
|
b2 = n2 / 48d + n3 / 15d;
|
||||||
|
b3 = n3 * 17d / 480d;
|
||||||
|
d1 = n * 2d - n2 * 2d / 3d - n3 * 2d;
|
||||||
|
d2 = n2 * 7d / 3d - n3 * 8d / 5d;
|
||||||
|
d3 = n3 * 56d / 15d;
|
||||||
|
A = equatorialRadius / (1d + n) * (1d + n2 / 4d + n2 * n2 / 64d);
|
||||||
|
}
|
||||||
|
|
||||||
a1 = n / 2d - n2 * 2d / 3d + n3 * 5d / 16d;
|
public override double GridConvergence(double latitude, double longitude)
|
||||||
a2 = n2 * 13d / 48d - n3 * 3d / 5d;
|
{
|
||||||
a3 = n3 * 61d / 240d;
|
// φ
|
||||||
b1 = n / 2d - n2 * 2d / 3d + n3 * 37d / 96d;
|
var phi = latitude * Math.PI / 180d;
|
||||||
b2 = n2 / 48d + n3 / 15d;
|
// λ - λ0
|
||||||
b3 = n3 * 17d / 480d;
|
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
||||||
d1 = n * 2d - n2 * 2d / 3d - n3 * 2d;
|
|
||||||
d2 = n2 * 7d / 3d - n3 * 8d / 5d;
|
|
||||||
d3 = n3 * 56d / 15d;
|
|
||||||
A = equatorialRadius / (1d + n) * (1d + n2 / 4d + n2 * n2 / 64d);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override double GridConvergence(double latitude, double longitude)
|
// γ calculation for the sphere is sufficiently accurate
|
||||||
{
|
//
|
||||||
// φ
|
return Math.Atan(Math.Tan(lambda) * Math.Sin(phi)) * 180d / Math.PI;
|
||||||
var phi = latitude * Math.PI / 180d;
|
}
|
||||||
// λ - λ0
|
|
||||||
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
|
||||||
|
|
||||||
// γ calculation for the sphere is sufficiently accurate
|
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||||
//
|
{
|
||||||
return Math.Atan(Math.Tan(lambda) * Math.Sin(phi)) * 180d / Math.PI;
|
// φ
|
||||||
}
|
var phi = latitude * Math.PI / 180d;
|
||||||
|
var sinPhi = Math.Sin(phi);
|
||||||
|
// λ - λ0
|
||||||
|
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
||||||
|
var cosLambda = Math.Cos(lambda);
|
||||||
|
var tanLambda = Math.Tan(lambda);
|
||||||
|
// t
|
||||||
|
var t = Math.Sinh(Atanh(sinPhi) - m * Atanh(m * sinPhi));
|
||||||
|
var u = Math.Sqrt(1d + t * t);
|
||||||
|
// ξ'
|
||||||
|
var xi_ = Math.Atan2(t, cosLambda);
|
||||||
|
// η'
|
||||||
|
var eta_ = Atanh(Math.Sin(lambda) / u);
|
||||||
|
// σ
|
||||||
|
var sigma = 1 +
|
||||||
|
2d * a1 * Math.Cos(2d * xi_) * Math.Cosh(2d * eta_) +
|
||||||
|
4d * a2 * Math.Cos(4d * xi_) * Math.Cosh(4d * eta_) +
|
||||||
|
6d * a3 * Math.Cos(6d * xi_) * Math.Cosh(6d * eta_);
|
||||||
|
// τ
|
||||||
|
var tau =
|
||||||
|
2d * a1 * Math.Sin(2d * xi_) * Math.Sinh(2d * eta_) +
|
||||||
|
4d * a2 * Math.Sin(4d * xi_) * Math.Sinh(4d * eta_) +
|
||||||
|
6d * a3 * Math.Sin(6d * xi_) * Math.Sinh(6d * eta_);
|
||||||
|
|
||||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
var q = (1d - n) / (1d + n) * Math.Tan(phi);
|
||||||
{
|
var k = ScaleFactor * A / EquatorialRadius
|
||||||
// φ
|
* Math.Sqrt((1d + q * q) * (sigma * sigma + tau * tau) / (t * t + cosLambda * cosLambda));
|
||||||
var phi = latitude * Math.PI / 180d;
|
|
||||||
var sinPhi = Math.Sin(phi);
|
|
||||||
// λ - λ0
|
|
||||||
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
|
||||||
var cosLambda = Math.Cos(lambda);
|
|
||||||
var tanLambda = Math.Tan(lambda);
|
|
||||||
// t
|
|
||||||
var t = Math.Sinh(Atanh(sinPhi) - m * Atanh(m * sinPhi));
|
|
||||||
var u = Math.Sqrt(1d + t * t);
|
|
||||||
// ξ'
|
|
||||||
var xi_ = Math.Atan2(t, cosLambda);
|
|
||||||
// η'
|
|
||||||
var eta_ = Atanh(Math.Sin(lambda) / u);
|
|
||||||
// σ
|
|
||||||
var sigma = 1 +
|
|
||||||
2d * a1 * Math.Cos(2d * xi_) * Math.Cosh(2d * eta_) +
|
|
||||||
4d * a2 * Math.Cos(4d * xi_) * Math.Cosh(4d * eta_) +
|
|
||||||
6d * a3 * Math.Cos(6d * xi_) * Math.Cosh(6d * eta_);
|
|
||||||
// τ
|
|
||||||
var tau =
|
|
||||||
2d * a1 * Math.Sin(2d * xi_) * Math.Sinh(2d * eta_) +
|
|
||||||
4d * a2 * Math.Sin(4d * xi_) * Math.Sinh(4d * eta_) +
|
|
||||||
6d * a3 * Math.Sin(6d * xi_) * Math.Sinh(6d * eta_);
|
|
||||||
|
|
||||||
var q = (1d - n) / (1d + n) * Math.Tan(phi);
|
// γ, grid convergence
|
||||||
var k = ScaleFactor * A / EquatorialRadius
|
var gamma = Math.Atan2(tau * u + sigma * t * tanLambda, sigma * u - tau * t * tanLambda);
|
||||||
* Math.Sqrt((1d + q * q) * (sigma * sigma + tau * tau) / (t * t + cosLambda * cosLambda));
|
|
||||||
|
|
||||||
// γ, grid convergence
|
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
|
||||||
var gamma = Math.Atan2(tau * u + sigma * t * tanLambda, sigma * u - tau * t * tanLambda);
|
transform.Rotate(-gamma * 180d / Math.PI);
|
||||||
|
|
||||||
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
|
return transform;
|
||||||
transform.Rotate(-gamma * 180d / Math.PI);
|
}
|
||||||
|
|
||||||
return transform;
|
public override Point LocationToMap(double latitude, double longitude)
|
||||||
}
|
{
|
||||||
|
// φ
|
||||||
|
var phi = latitude * Math.PI / 180d;
|
||||||
|
var sinPhi = Math.Sin(phi);
|
||||||
|
// t
|
||||||
|
var t = Math.Sinh(Atanh(sinPhi) - m * Atanh(m * sinPhi));
|
||||||
|
// λ - λ0
|
||||||
|
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
||||||
|
// ξ'
|
||||||
|
var xi_ = Math.Atan2(t, Math.Cos(lambda));
|
||||||
|
// η'
|
||||||
|
var eta_ = Atanh(Math.Sin(lambda) / Math.Sqrt(1d + t * t));
|
||||||
|
|
||||||
public override Point LocationToMap(double latitude, double longitude)
|
var x = FalseEasting + ScaleFactor * A * (eta_ +
|
||||||
{
|
a1 * Math.Cos(2d * xi_) * Math.Sinh(2d * eta_) +
|
||||||
// φ
|
a2 * Math.Cos(4d * xi_) * Math.Sinh(4d * eta_) +
|
||||||
var phi = latitude * Math.PI / 180d;
|
a3 * Math.Cos(6d * xi_) * Math.Sinh(6d * eta_));
|
||||||
var sinPhi = Math.Sin(phi);
|
|
||||||
// t
|
|
||||||
var t = Math.Sinh(Atanh(sinPhi) - m * Atanh(m * sinPhi));
|
|
||||||
// λ - λ0
|
|
||||||
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
|
||||||
// ξ'
|
|
||||||
var xi_ = Math.Atan2(t, Math.Cos(lambda));
|
|
||||||
// η'
|
|
||||||
var eta_ = Atanh(Math.Sin(lambda) / Math.Sqrt(1d + t * t));
|
|
||||||
|
|
||||||
var x = FalseEasting + ScaleFactor * A * (eta_ +
|
var y = FalseNorthing + ScaleFactor * A * (xi_ +
|
||||||
a1 * Math.Cos(2d * xi_) * Math.Sinh(2d * eta_) +
|
a1 * Math.Sin(2d * xi_) * Math.Cosh(2d * eta_) +
|
||||||
a2 * Math.Cos(4d * xi_) * Math.Sinh(4d * eta_) +
|
a2 * Math.Sin(4d * xi_) * Math.Cosh(4d * eta_) +
|
||||||
a3 * Math.Cos(6d * xi_) * Math.Sinh(6d * eta_));
|
a3 * Math.Sin(6d * xi_) * Math.Cosh(6d * eta_));
|
||||||
|
|
||||||
var y = FalseNorthing + ScaleFactor * A * (xi_ +
|
return new Point(x, y);
|
||||||
a1 * Math.Sin(2d * xi_) * Math.Cosh(2d * eta_) +
|
}
|
||||||
a2 * Math.Sin(4d * xi_) * Math.Cosh(4d * eta_) +
|
|
||||||
a3 * Math.Sin(6d * xi_) * Math.Cosh(6d * eta_));
|
|
||||||
|
|
||||||
return new Point(x, y);
|
public override Location MapToLocation(double x, double y)
|
||||||
}
|
{
|
||||||
|
// ξ
|
||||||
|
var xi = (y - FalseNorthing) / (ScaleFactor * A);
|
||||||
|
// η
|
||||||
|
var eta = (x - FalseEasting) / (ScaleFactor * A);
|
||||||
|
// ξ'
|
||||||
|
var xi_ = xi -
|
||||||
|
b1 * Math.Sin(2d * xi) * Math.Cosh(2d * eta) -
|
||||||
|
b2 * Math.Sin(4d * xi) * Math.Cosh(4d * eta) -
|
||||||
|
b3 * Math.Sin(6d * xi) * Math.Cosh(6d * eta);
|
||||||
|
// η'
|
||||||
|
var eta_ = eta -
|
||||||
|
b1 * Math.Cos(2d * xi) * Math.Sinh(2d * eta) -
|
||||||
|
b2 * Math.Cos(4d * xi) * Math.Sinh(4d * eta) -
|
||||||
|
b3 * Math.Cos(6d * xi) * Math.Sinh(6d * eta);
|
||||||
|
// χ
|
||||||
|
var chi = Math.Asin(Math.Sin(xi_) / Math.Cosh(eta_));
|
||||||
|
// φ
|
||||||
|
var phi = chi +
|
||||||
|
d1 * Math.Sin(2d * chi) +
|
||||||
|
d2 * Math.Sin(4d * chi) +
|
||||||
|
d3 * Math.Sin(6d * chi);
|
||||||
|
// λ - λ0
|
||||||
|
var lambda = Math.Atan2(Math.Sinh(eta_), Math.Cos(xi_));
|
||||||
|
|
||||||
public override Location MapToLocation(double x, double y)
|
return new Location(
|
||||||
{
|
phi * 180d / Math.PI,
|
||||||
// ξ
|
lambda * 180d / Math.PI + CentralMeridian);
|
||||||
var xi = (y - FalseNorthing) / (ScaleFactor * A);
|
}
|
||||||
// η
|
|
||||||
var eta = (x - FalseEasting) / (ScaleFactor * A);
|
|
||||||
// ξ'
|
|
||||||
var xi_ = xi -
|
|
||||||
b1 * Math.Sin(2d * xi) * Math.Cosh(2d * eta) -
|
|
||||||
b2 * Math.Sin(4d * xi) * Math.Cosh(4d * eta) -
|
|
||||||
b3 * Math.Sin(6d * xi) * Math.Cosh(6d * eta);
|
|
||||||
// η'
|
|
||||||
var eta_ = eta -
|
|
||||||
b1 * Math.Cos(2d * xi) * Math.Sinh(2d * eta) -
|
|
||||||
b2 * Math.Cos(4d * xi) * Math.Sinh(4d * eta) -
|
|
||||||
b3 * Math.Cos(6d * xi) * Math.Sinh(6d * eta);
|
|
||||||
// χ
|
|
||||||
var chi = Math.Asin(Math.Sin(xi_) / Math.Cosh(eta_));
|
|
||||||
// φ
|
|
||||||
var phi = chi +
|
|
||||||
d1 * Math.Sin(2d * chi) +
|
|
||||||
d2 * Math.Sin(4d * chi) +
|
|
||||||
d3 * Math.Sin(6d * chi);
|
|
||||||
// λ - λ0
|
|
||||||
var lambda = Math.Atan2(Math.Sinh(eta_), Math.Cos(xi_));
|
|
||||||
|
|
||||||
return new Location(
|
|
||||||
phi * 180d / Math.PI,
|
|
||||||
lambda * 180d / Math.PI + CentralMeridian);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
private static double Atanh(double x) => Math.Log((1d + x) / (1d - x)) / 2d;
|
private static double Atanh(double x) => Math.Log((1d + x) / (1d - x)) / 2d;
|
||||||
#else
|
#else
|
||||||
private static double Atanh(double x) => Math.Atanh(x);
|
private static double Atanh(double x) => Math.Atanh(x);
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,160 +6,159 @@ using System.Windows.Media;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Elliptical Transverse Mercator Projection.
|
||||||
|
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.60-64,
|
||||||
|
/// and https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Convergence.
|
||||||
|
/// </summary>
|
||||||
|
public class TransverseMercatorProjectionSnyder : MapProjection
|
||||||
{
|
{
|
||||||
/// <summary>
|
public override double GridConvergence(double latitude, double longitude)
|
||||||
/// Elliptical Transverse Mercator Projection.
|
|
||||||
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.60-64,
|
|
||||||
/// and https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Convergence.
|
|
||||||
/// </summary>
|
|
||||||
public class TransverseMercatorProjectionSnyder : MapProjection
|
|
||||||
{
|
{
|
||||||
public override double GridConvergence(double latitude, double longitude)
|
// φ
|
||||||
|
var phi = latitude * Math.PI / 180d;
|
||||||
|
// λ - λ0
|
||||||
|
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
||||||
|
|
||||||
|
// γ calculation for the sphere is sufficiently accurate
|
||||||
|
//
|
||||||
|
return Math.Atan(Math.Tan(lambda) * Math.Sin(phi)) * 180d / Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
var k = ScaleFactor;
|
||||||
|
var gamma = 0d; // γ
|
||||||
|
|
||||||
|
if (latitude > -90d && latitude < 90d)
|
||||||
{
|
{
|
||||||
// φ
|
|
||||||
var phi = latitude * Math.PI / 180d;
|
var phi = latitude * Math.PI / 180d;
|
||||||
// λ - λ0
|
var sinPhi = Math.Sin(phi);
|
||||||
|
var cosPhi = Math.Cos(phi);
|
||||||
|
var tanPhi = sinPhi / cosPhi;
|
||||||
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
||||||
|
|
||||||
// γ calculation for the sphere is sufficiently accurate
|
|
||||||
//
|
|
||||||
return Math.Atan(Math.Tan(lambda) * Math.Sin(phi)) * 180d / Math.PI;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
|
||||||
{
|
|
||||||
var k = ScaleFactor;
|
|
||||||
var gamma = 0d; // γ
|
|
||||||
|
|
||||||
if (latitude > -90d && latitude < 90d)
|
|
||||||
{
|
|
||||||
var phi = latitude * Math.PI / 180d;
|
|
||||||
var sinPhi = Math.Sin(phi);
|
|
||||||
var cosPhi = Math.Cos(phi);
|
|
||||||
var tanPhi = sinPhi / cosPhi;
|
|
||||||
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
|
||||||
|
|
||||||
var e2 = (2d - Flattening) * Flattening;
|
|
||||||
var e_2 = e2 / (1d - e2); // (8-12)
|
|
||||||
var T = tanPhi * tanPhi; // (8-13)
|
|
||||||
var C = e_2 * cosPhi * cosPhi; // (8-14)
|
|
||||||
var A = lambda * cosPhi; // (8-15)
|
|
||||||
var A2 = A * A;
|
|
||||||
var A4 = A2 * A2;
|
|
||||||
var A6 = A2 * A4;
|
|
||||||
|
|
||||||
k *= 1d + (1d + C) * A2 / 2d +
|
|
||||||
(5d - 4d * T + 42d * C + 13d * C * C - 28d * e_2) * A4 / 24d +
|
|
||||||
(61d - 148d * T + 16 * T * T) * A6 / 720d; // (8-11)
|
|
||||||
|
|
||||||
gamma = Math.Atan(Math.Tan(lambda) * sinPhi) * 180d / Math.PI;
|
|
||||||
}
|
|
||||||
|
|
||||||
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
|
|
||||||
transform.Rotate(-gamma);
|
|
||||||
|
|
||||||
return transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Point LocationToMap(double latitude, double longitude)
|
|
||||||
{
|
|
||||||
var phi = latitude * Math.PI / 180d;
|
|
||||||
var M = MeridianDistance(phi);
|
|
||||||
var M0 = MeridianDistance(LatitudeOfOrigin * Math.PI / 180d);
|
|
||||||
double x, y;
|
|
||||||
|
|
||||||
if (latitude > -90d && latitude < 90d)
|
|
||||||
{
|
|
||||||
var sinPhi = Math.Sin(phi);
|
|
||||||
var cosPhi = Math.Cos(phi);
|
|
||||||
var tanPhi = sinPhi / cosPhi;
|
|
||||||
|
|
||||||
var e2 = (2d - Flattening) * Flattening;
|
|
||||||
var e_2 = e2 / (1d - e2); // (8-12)
|
|
||||||
var N = EquatorialRadius / Math.Sqrt(1d - e2 * sinPhi * sinPhi); // (4-20)
|
|
||||||
var T = tanPhi * tanPhi; // (8-13)
|
|
||||||
var C = e_2 * cosPhi * cosPhi; // (8-14)
|
|
||||||
var A = (longitude - CentralMeridian) * Math.PI / 180d * cosPhi; // (8-15)
|
|
||||||
var A2 = A * A;
|
|
||||||
var A3 = A * A2;
|
|
||||||
var A4 = A * A3;
|
|
||||||
var A5 = A * A4;
|
|
||||||
var A6 = A * A5;
|
|
||||||
|
|
||||||
x = ScaleFactor * N *
|
|
||||||
(A + (1d - T + C) * A3 / 6d + (5d - 18d * T + T * T + 72d * C - 58d * e_2) * A5 / 120d); // (8-9)
|
|
||||||
y = ScaleFactor * (M - M0 + N * tanPhi * (A2 / 2d + (5d - T + 9d * C + 4d * C * C) * A4 / 24d +
|
|
||||||
(61d - 58d * T + T * T + 600d * C - 330d * e_2) * A6 / 720d)); // (8-10)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x = 0d;
|
|
||||||
y = ScaleFactor * (M - M0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Point(x + FalseEasting, y + FalseNorthing);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Location MapToLocation(double x, double y)
|
|
||||||
{
|
|
||||||
var e2 = (2d - Flattening) * Flattening;
|
var e2 = (2d - Flattening) * Flattening;
|
||||||
var e4 = e2 * e2;
|
|
||||||
var e6 = e2 * e4;
|
|
||||||
var s = Math.Sqrt(1d - e2);
|
|
||||||
var e1 = (1d - s) / (1d + s); // (3-24)
|
|
||||||
var e12 = e1 * e1;
|
|
||||||
var e13 = e1 * e12;
|
|
||||||
var e14 = e1 * e13;
|
|
||||||
|
|
||||||
var M0 = MeridianDistance(LatitudeOfOrigin * Math.PI / 180d);
|
|
||||||
var M = M0 + (y - FalseNorthing) / ScaleFactor; // (8-20)
|
|
||||||
var mu = M / (EquatorialRadius * (1d - e2 / 4d - e4 * 3d / 64d - e6 * 5d / 256d)); // (7-19)
|
|
||||||
var phi0 = mu +
|
|
||||||
(e1 * 3d / 2d - e13 * 27d / 32d) * Math.Sin(2d * mu) +
|
|
||||||
(e12 * 21d / 16d - e14 * 55d / 32d) * Math.Sin(4d * mu) +
|
|
||||||
e13 * 151d / 96d * Math.Sin(6d * mu) +
|
|
||||||
e14 * 1097d / 512d * Math.Sin(8d * mu); // (3-26)
|
|
||||||
|
|
||||||
var sinPhi0 = Math.Sin(phi0);
|
|
||||||
var cosPhi0 = Math.Cos(phi0);
|
|
||||||
var tanPhi0 = sinPhi0 / cosPhi0;
|
|
||||||
|
|
||||||
var e_2 = e2 / (1d - e2); // (8-12)
|
var e_2 = e2 / (1d - e2); // (8-12)
|
||||||
var C1 = e_2 * cosPhi0 * cosPhi0; // (8-21)
|
var T = tanPhi * tanPhi; // (8-13)
|
||||||
var T1 = sinPhi0 * sinPhi0 / (cosPhi0 * cosPhi0); // (8-22)
|
var C = e_2 * cosPhi * cosPhi; // (8-14)
|
||||||
s = Math.Sqrt(1d - e2 * sinPhi0 * sinPhi0);
|
var A = lambda * cosPhi; // (8-15)
|
||||||
var N1 = EquatorialRadius / s; // (8-23)
|
var A2 = A * A;
|
||||||
var R1 = EquatorialRadius * (1d - e2) / (s * s * s); // (8-24)
|
var A4 = A2 * A2;
|
||||||
var D = (x - FalseEasting) / (N1 * ScaleFactor); // (8-25)
|
var A6 = A2 * A4;
|
||||||
var D2 = D * D;
|
|
||||||
var D3 = D * D2;
|
|
||||||
var D4 = D * D3;
|
|
||||||
var D5 = D * D4;
|
|
||||||
var D6 = D * D5;
|
|
||||||
|
|
||||||
var phi = phi0 - N1 * tanPhi0 / R1 * (D2 / 2d - (5d + 3d * T1 + 10d * C1 - 4d * C1 * C1 - 9d * e_2) * D4 / 24d +
|
k *= 1d + (1d + C) * A2 / 2d +
|
||||||
(61d + 90d * T1 + 45d * T1 * T1 + 298 * C1 - 3d * C1 * C1 - 252d * e_2) * D6 / 720d); // (8-17)
|
(5d - 4d * T + 42d * C + 13d * C * C - 28d * e_2) * A4 / 24d +
|
||||||
|
(61d - 148d * T + 16 * T * T) * A6 / 720d; // (8-11)
|
||||||
|
|
||||||
var lambda = (D - (1d + 2d * T1 + C1) * D3 / 6d +
|
gamma = Math.Atan(Math.Tan(lambda) * sinPhi) * 180d / Math.PI;
|
||||||
(5d - 2d * C1 - 3d * C1 * C1 + 28d * T1 + 24d * T1 * T1 + 8d * e_2) * D5 / 120d) / cosPhi0; // (8-18)
|
|
||||||
|
|
||||||
return new Location(
|
|
||||||
phi * 180d / Math.PI,
|
|
||||||
lambda * 180d / Math.PI + CentralMeridian);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double MeridianDistance(double phi)
|
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
|
||||||
|
transform.Rotate(-gamma);
|
||||||
|
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Point LocationToMap(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
var phi = latitude * Math.PI / 180d;
|
||||||
|
var M = MeridianDistance(phi);
|
||||||
|
var M0 = MeridianDistance(LatitudeOfOrigin * Math.PI / 180d);
|
||||||
|
double x, y;
|
||||||
|
|
||||||
|
if (latitude > -90d && latitude < 90d)
|
||||||
{
|
{
|
||||||
var e2 = (2d - Flattening) * Flattening;
|
var sinPhi = Math.Sin(phi);
|
||||||
var e4 = e2 * e2;
|
var cosPhi = Math.Cos(phi);
|
||||||
var e6 = e2 * e4;
|
var tanPhi = sinPhi / cosPhi;
|
||||||
|
|
||||||
return EquatorialRadius * (
|
var e2 = (2d - Flattening) * Flattening;
|
||||||
(1d - e2 / 4d - e4 * 3d / 64d - e6 * 5d / 256d) * phi -
|
var e_2 = e2 / (1d - e2); // (8-12)
|
||||||
(e2 * 3d / 8d + e4 * 3d / 32d + e6 * 45d / 1024d) * Math.Sin(2d * phi) +
|
var N = EquatorialRadius / Math.Sqrt(1d - e2 * sinPhi * sinPhi); // (4-20)
|
||||||
(e4 * 15d / 256d + e6 * 45d / 1024d) * Math.Sin(4d * phi) -
|
var T = tanPhi * tanPhi; // (8-13)
|
||||||
e6 * 35d / 3072d * Math.Sin(6d * phi)); // (3-21)
|
var C = e_2 * cosPhi * cosPhi; // (8-14)
|
||||||
|
var A = (longitude - CentralMeridian) * Math.PI / 180d * cosPhi; // (8-15)
|
||||||
|
var A2 = A * A;
|
||||||
|
var A3 = A * A2;
|
||||||
|
var A4 = A * A3;
|
||||||
|
var A5 = A * A4;
|
||||||
|
var A6 = A * A5;
|
||||||
|
|
||||||
|
x = ScaleFactor * N *
|
||||||
|
(A + (1d - T + C) * A3 / 6d + (5d - 18d * T + T * T + 72d * C - 58d * e_2) * A5 / 120d); // (8-9)
|
||||||
|
y = ScaleFactor * (M - M0 + N * tanPhi * (A2 / 2d + (5d - T + 9d * C + 4d * C * C) * A4 / 24d +
|
||||||
|
(61d - 58d * T + T * T + 600d * C - 330d * e_2) * A6 / 720d)); // (8-10)
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
x = 0d;
|
||||||
|
y = ScaleFactor * (M - M0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Point(x + FalseEasting, y + FalseNorthing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Location MapToLocation(double x, double y)
|
||||||
|
{
|
||||||
|
var e2 = (2d - Flattening) * Flattening;
|
||||||
|
var e4 = e2 * e2;
|
||||||
|
var e6 = e2 * e4;
|
||||||
|
var s = Math.Sqrt(1d - e2);
|
||||||
|
var e1 = (1d - s) / (1d + s); // (3-24)
|
||||||
|
var e12 = e1 * e1;
|
||||||
|
var e13 = e1 * e12;
|
||||||
|
var e14 = e1 * e13;
|
||||||
|
|
||||||
|
var M0 = MeridianDistance(LatitudeOfOrigin * Math.PI / 180d);
|
||||||
|
var M = M0 + (y - FalseNorthing) / ScaleFactor; // (8-20)
|
||||||
|
var mu = M / (EquatorialRadius * (1d - e2 / 4d - e4 * 3d / 64d - e6 * 5d / 256d)); // (7-19)
|
||||||
|
var phi0 = mu +
|
||||||
|
(e1 * 3d / 2d - e13 * 27d / 32d) * Math.Sin(2d * mu) +
|
||||||
|
(e12 * 21d / 16d - e14 * 55d / 32d) * Math.Sin(4d * mu) +
|
||||||
|
e13 * 151d / 96d * Math.Sin(6d * mu) +
|
||||||
|
e14 * 1097d / 512d * Math.Sin(8d * mu); // (3-26)
|
||||||
|
|
||||||
|
var sinPhi0 = Math.Sin(phi0);
|
||||||
|
var cosPhi0 = Math.Cos(phi0);
|
||||||
|
var tanPhi0 = sinPhi0 / cosPhi0;
|
||||||
|
|
||||||
|
var e_2 = e2 / (1d - e2); // (8-12)
|
||||||
|
var C1 = e_2 * cosPhi0 * cosPhi0; // (8-21)
|
||||||
|
var T1 = sinPhi0 * sinPhi0 / (cosPhi0 * cosPhi0); // (8-22)
|
||||||
|
s = Math.Sqrt(1d - e2 * sinPhi0 * sinPhi0);
|
||||||
|
var N1 = EquatorialRadius / s; // (8-23)
|
||||||
|
var R1 = EquatorialRadius * (1d - e2) / (s * s * s); // (8-24)
|
||||||
|
var D = (x - FalseEasting) / (N1 * ScaleFactor); // (8-25)
|
||||||
|
var D2 = D * D;
|
||||||
|
var D3 = D * D2;
|
||||||
|
var D4 = D * D3;
|
||||||
|
var D5 = D * D4;
|
||||||
|
var D6 = D * D5;
|
||||||
|
|
||||||
|
var phi = phi0 - N1 * tanPhi0 / R1 * (D2 / 2d - (5d + 3d * T1 + 10d * C1 - 4d * C1 * C1 - 9d * e_2) * D4 / 24d +
|
||||||
|
(61d + 90d * T1 + 45d * T1 * T1 + 298 * C1 - 3d * C1 * C1 - 252d * e_2) * D6 / 720d); // (8-17)
|
||||||
|
|
||||||
|
var lambda = (D - (1d + 2d * T1 + C1) * D3 / 6d +
|
||||||
|
(5d - 2d * C1 - 3d * C1 * C1 + 28d * T1 + 24d * T1 * T1 + 8d * e_2) * D5 / 120d) / cosPhi0; // (8-18)
|
||||||
|
|
||||||
|
return new Location(
|
||||||
|
phi * 180d / Math.PI,
|
||||||
|
lambda * 180d / Math.PI + CentralMeridian);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double MeridianDistance(double phi)
|
||||||
|
{
|
||||||
|
var e2 = (2d - Flattening) * Flattening;
|
||||||
|
var e4 = e2 * e2;
|
||||||
|
var e6 = e2 * e4;
|
||||||
|
|
||||||
|
return EquatorialRadius * (
|
||||||
|
(1d - e2 / 4d - e4 * 3d / 64d - e6 * 5d / 256d) * phi -
|
||||||
|
(e2 * 3d / 8d + e4 * 3d / 32d + e6 * 45d / 1024d) * Math.Sin(2d * phi) +
|
||||||
|
(e4 * 15d / 256d + e6 * 45d / 1024d) * Math.Sin(4d * phi) -
|
||||||
|
e6 * 35d / 3072d * Math.Sin(6d * phi)); // (3-21)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,120 +16,119 @@ using ConverterCulture = System.String;
|
||||||
using ConverterCulture = System.Globalization.CultureInfo;
|
using ConverterCulture = System.Globalization.CultureInfo;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class LocationConverter : TypeConverter, IValueConverter
|
||||||
{
|
{
|
||||||
public partial class LocationConverter : TypeConverter, IValueConverter
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||||
{
|
{
|
||||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
return sourceType == typeof(string);
|
||||||
{
|
|
||||||
return sourceType == typeof(string);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
|
||||||
{
|
|
||||||
return Location.Parse(value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, ConverterCulture culture)
|
|
||||||
{
|
|
||||||
return ConvertFrom(value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, ConverterCulture culture)
|
|
||||||
{
|
|
||||||
return value.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class LocationCollectionConverter : TypeConverter, IValueConverter
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||||
{
|
{
|
||||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
return Location.Parse(value.ToString());
|
||||||
{
|
|
||||||
return sourceType == typeof(string);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
|
||||||
{
|
|
||||||
return LocationCollection.Parse(value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, ConverterCulture culture)
|
|
||||||
{
|
|
||||||
return ConvertFrom(value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, ConverterCulture culture)
|
|
||||||
{
|
|
||||||
return value.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class BoundingBoxConverter : TypeConverter, IValueConverter
|
public object Convert(object value, Type targetType, object parameter, ConverterCulture culture)
|
||||||
{
|
{
|
||||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
return ConvertFrom(value.ToString());
|
||||||
{
|
|
||||||
return sourceType == typeof(string);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
|
||||||
{
|
|
||||||
return BoundingBox.Parse(value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, ConverterCulture culture)
|
|
||||||
{
|
|
||||||
return ConvertFrom(value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, ConverterCulture culture)
|
|
||||||
{
|
|
||||||
return value.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class TileSourceConverter : TypeConverter, IValueConverter
|
public object ConvertBack(object value, Type targetType, object parameter, ConverterCulture culture)
|
||||||
{
|
{
|
||||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
return value.ToString();
|
||||||
{
|
}
|
||||||
return sourceType == typeof(string);
|
}
|
||||||
}
|
|
||||||
|
public partial class LocationCollectionConverter : TypeConverter, IValueConverter
|
||||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
{
|
||||||
{
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||||
return TileSource.Parse(value.ToString());
|
{
|
||||||
}
|
return sourceType == typeof(string);
|
||||||
|
}
|
||||||
public object Convert(object value, Type targetType, object parameter, ConverterCulture culture)
|
|
||||||
{
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||||
return ConvertFrom(value.ToString());
|
{
|
||||||
}
|
return LocationCollection.Parse(value.ToString());
|
||||||
|
}
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, ConverterCulture culture)
|
|
||||||
{
|
public object Convert(object value, Type targetType, object parameter, ConverterCulture culture)
|
||||||
return value.ToString();
|
{
|
||||||
}
|
return ConvertFrom(value.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class MapProjectionConverter : TypeConverter, IValueConverter
|
public object ConvertBack(object value, Type targetType, object parameter, ConverterCulture culture)
|
||||||
{
|
{
|
||||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
return value.ToString();
|
||||||
{
|
}
|
||||||
return sourceType == typeof(string);
|
}
|
||||||
}
|
|
||||||
|
public partial class BoundingBoxConverter : TypeConverter, IValueConverter
|
||||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
{
|
||||||
{
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||||
return MapProjection.Parse(value.ToString());
|
{
|
||||||
}
|
return sourceType == typeof(string);
|
||||||
|
}
|
||||||
public object Convert(object value, Type targetType, object parameter, ConverterCulture culture)
|
|
||||||
{
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||||
return ConvertFrom(value.ToString());
|
{
|
||||||
}
|
return BoundingBox.Parse(value.ToString());
|
||||||
|
}
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, ConverterCulture culture)
|
|
||||||
{
|
public object Convert(object value, Type targetType, object parameter, ConverterCulture culture)
|
||||||
return value.ToString();
|
{
|
||||||
}
|
return ConvertFrom(value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, ConverterCulture culture)
|
||||||
|
{
|
||||||
|
return value.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class TileSourceConverter : TypeConverter, IValueConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||||
|
{
|
||||||
|
return sourceType == typeof(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||||
|
{
|
||||||
|
return TileSource.Parse(value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Convert(object value, Type targetType, object parameter, ConverterCulture culture)
|
||||||
|
{
|
||||||
|
return ConvertFrom(value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, ConverterCulture culture)
|
||||||
|
{
|
||||||
|
return value.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class MapProjectionConverter : TypeConverter, IValueConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||||
|
{
|
||||||
|
return sourceType == typeof(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||||
|
{
|
||||||
|
return MapProjection.Parse(value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Convert(object value, Type targetType, object parameter, ConverterCulture culture)
|
||||||
|
{
|
||||||
|
return ConvertFrom(value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, ConverterCulture culture)
|
||||||
|
{
|
||||||
|
return value.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,30 +14,29 @@ using Avalonia;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
{
|
|
||||||
public static class UIElementExtension
|
|
||||||
{
|
|
||||||
public static void SetRenderTransform(this UIElement element, Transform transform, bool center = false)
|
|
||||||
{
|
|
||||||
element.RenderTransform = transform;
|
|
||||||
#if AVALONIA
|
|
||||||
element.RenderTransformOrigin = center ? RelativePoint.Center : RelativePoint.TopLeft;
|
|
||||||
#else
|
|
||||||
if (center)
|
|
||||||
{
|
|
||||||
element.RenderTransformOrigin = new Point(0.5, 0.5);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SetVisible(this UIElement element, bool visible)
|
public static class UIElementExtension
|
||||||
{
|
{
|
||||||
|
public static void SetRenderTransform(this UIElement element, Transform transform, bool center = false)
|
||||||
|
{
|
||||||
|
element.RenderTransform = transform;
|
||||||
#if AVALONIA
|
#if AVALONIA
|
||||||
element.IsVisible = visible;
|
element.RenderTransformOrigin = center ? RelativePoint.Center : RelativePoint.TopLeft;
|
||||||
#else
|
#else
|
||||||
element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
if (center)
|
||||||
#endif
|
{
|
||||||
|
element.RenderTransformOrigin = new Point(0.5, 0.5);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetVisible(this UIElement element, bool visible)
|
||||||
|
{
|
||||||
|
#if AVALONIA
|
||||||
|
element.IsVisible = visible;
|
||||||
|
#else
|
||||||
|
element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,20 @@ using Microsoft.UI.Xaml;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
{
|
|
||||||
internal class UpdateTimer : DispatcherTimer
|
|
||||||
{
|
|
||||||
public void Run(bool restart = false)
|
|
||||||
{
|
|
||||||
if (restart)
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsEnabled)
|
internal class UpdateTimer : DispatcherTimer
|
||||||
{
|
{
|
||||||
Start();
|
public void Run(bool restart = false)
|
||||||
}
|
{
|
||||||
|
if (restart)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
Start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,53 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class UriTileSource : TileSource
|
||||||
{
|
{
|
||||||
public class UriTileSource : TileSource
|
private string uriFormat;
|
||||||
|
|
||||||
|
public string UriTemplate
|
||||||
{
|
{
|
||||||
private string uriFormat;
|
get;
|
||||||
|
set
|
||||||
public string UriTemplate
|
|
||||||
{
|
{
|
||||||
get;
|
field = value;
|
||||||
set
|
uriFormat = field
|
||||||
|
.Replace("{z}", "{0}")
|
||||||
|
.Replace("{x}", "{1}")
|
||||||
|
.Replace("{y}", "{2}")
|
||||||
|
.Replace("{s}", "{3}");
|
||||||
|
|
||||||
|
if (Subdomains == null && field.Contains("{s}"))
|
||||||
{
|
{
|
||||||
field = value;
|
Subdomains = ["a", "b", "c"]; // default OpenStreetMap subdomains
|
||||||
uriFormat = field
|
|
||||||
.Replace("{z}", "{0}")
|
|
||||||
.Replace("{x}", "{1}")
|
|
||||||
.Replace("{y}", "{2}")
|
|
||||||
.Replace("{s}", "{3}");
|
|
||||||
|
|
||||||
if (Subdomains == null && field.Contains("{s}"))
|
|
||||||
{
|
|
||||||
Subdomains = ["a", "b", "c"]; // default OpenStreetMap subdomains
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] Subdomains { get; set; }
|
|
||||||
|
|
||||||
public override Uri GetUri(int zoomLevel, int column, int row)
|
|
||||||
{
|
|
||||||
Uri uri = null;
|
|
||||||
|
|
||||||
if (uriFormat != null)
|
|
||||||
{
|
|
||||||
var uriString = Subdomains?.Length > 0
|
|
||||||
? string.Format(uriFormat, zoomLevel, column, row, Subdomains[(column + row) % Subdomains.Length])
|
|
||||||
: string.Format(uriFormat, zoomLevel, column, row);
|
|
||||||
|
|
||||||
uri = new Uri(uriString, UriKind.RelativeOrAbsolute);
|
|
||||||
}
|
|
||||||
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TmsTileSource : UriTileSource
|
public string[] Subdomains { get; set; }
|
||||||
|
|
||||||
|
public override Uri GetUri(int zoomLevel, int column, int row)
|
||||||
{
|
{
|
||||||
public override Uri GetUri(int zoomLevel, int column, int row)
|
Uri uri = null;
|
||||||
|
|
||||||
|
if (uriFormat != null)
|
||||||
{
|
{
|
||||||
return base.GetUri(zoomLevel, column, (1 << zoomLevel) - 1 - row);
|
var uriString = Subdomains?.Length > 0
|
||||||
|
? string.Format(uriFormat, zoomLevel, column, row, Subdomains[(column + row) % Subdomains.Length])
|
||||||
|
: string.Format(uriFormat, zoomLevel, column, row);
|
||||||
|
|
||||||
|
uri = new Uri(uriString, UriKind.RelativeOrAbsolute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TmsTileSource : UriTileSource
|
||||||
|
{
|
||||||
|
public override Uri GetUri(int zoomLevel, int column, int row)
|
||||||
|
{
|
||||||
|
return base.GetUri(zoomLevel, column, (1 << zoomLevel) - 1 - row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,82 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class UtmProjection : TransverseMercatorProjection
|
||||||
{
|
{
|
||||||
public class UtmProjection : TransverseMercatorProjection
|
public UtmProjection(string crsId, double equatorialRadius, double flattening, int zone, bool north = true)
|
||||||
|
: base(equatorialRadius, flattening)
|
||||||
{
|
{
|
||||||
public UtmProjection(string crsId, double equatorialRadius, double flattening, int zone, bool north = true)
|
CrsId = crsId;
|
||||||
: base(equatorialRadius, flattening)
|
ScaleFactor = 0.9996;
|
||||||
{
|
CentralMeridian = zone * 6 - 183;
|
||||||
CrsId = crsId;
|
FalseEasting = 5e5;
|
||||||
ScaleFactor = 0.9996;
|
FalseNorthing = north ? 0d : 1e7;
|
||||||
CentralMeridian = zone * 6 - 183;
|
Zone = zone;
|
||||||
FalseEasting = 5e5;
|
|
||||||
FalseNorthing = north ? 0d : 1e7;
|
|
||||||
Zone = zone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Zone { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public int Zone { get; }
|
||||||
/// WGS84 Universal Transverse Mercator Projection -
|
}
|
||||||
/// EPSG:32601 to EPSG:32660 and EPSG:32701 to EPSG:32760.
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public class Wgs84UtmProjection : UtmProjection
|
/// WGS84 Universal Transverse Mercator Projection -
|
||||||
|
/// EPSG:32601 to EPSG:32660 and EPSG:32701 to EPSG:32760.
|
||||||
|
/// </summary>
|
||||||
|
public class Wgs84UtmProjection : UtmProjection
|
||||||
|
{
|
||||||
|
public const int FirstZone = 1;
|
||||||
|
public const int LastZone = 60;
|
||||||
|
public const int FirstZoneNorthEpsgCode = 32600 + FirstZone;
|
||||||
|
public const int LastZoneNorthEpsgCode = 32600 + LastZone;
|
||||||
|
public const int FirstZoneSouthEpsgCode = 32700 + FirstZone;
|
||||||
|
public const int LastZoneSouthEpsgCode = 32700 + LastZone;
|
||||||
|
|
||||||
|
public Wgs84UtmProjection(int zone, bool north)
|
||||||
|
: base($"EPSG:{(north ? 32600 : 32700) + zone}", Wgs84EquatorialRadius, Wgs84Flattening, zone, north)
|
||||||
{
|
{
|
||||||
public const int FirstZone = 1;
|
if (zone < FirstZone || zone > LastZone)
|
||||||
public const int LastZone = 60;
|
|
||||||
public const int FirstZoneNorthEpsgCode = 32600 + FirstZone;
|
|
||||||
public const int LastZoneNorthEpsgCode = 32600 + LastZone;
|
|
||||||
public const int FirstZoneSouthEpsgCode = 32700 + FirstZone;
|
|
||||||
public const int LastZoneSouthEpsgCode = 32700 + LastZone;
|
|
||||||
|
|
||||||
public Wgs84UtmProjection(int zone, bool north)
|
|
||||||
: base($"EPSG:{(north ? 32600 : 32700) + zone}", Wgs84EquatorialRadius, Wgs84Flattening, zone, north)
|
|
||||||
{
|
{
|
||||||
if (zone < FirstZone || zone > LastZone)
|
throw new ArgumentException($"Invalid WGS84 UTM zone {zone}.", nameof(zone));
|
||||||
{
|
}
|
||||||
throw new ArgumentException($"Invalid WGS84 UTM zone {zone}.", nameof(zone));
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
/// <summary>
|
||||||
|
/// ETRS89 Universal Transverse Mercator Projection - EPSG:25828 to EPSG:25838.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// ETRS89 Universal Transverse Mercator Projection - EPSG:25828 to EPSG:25838.
|
public class Etrs89UtmProjection : UtmProjection
|
||||||
/// </summary>
|
{
|
||||||
public class Etrs89UtmProjection : UtmProjection
|
public const int FirstZone = 28;
|
||||||
{
|
public const int LastZone = 38;
|
||||||
public const int FirstZone = 28;
|
public const int FirstZoneEpsgCode = 25800 + FirstZone;
|
||||||
public const int LastZone = 38;
|
public const int LastZoneEpsgCode = 25800 + LastZone;
|
||||||
public const int FirstZoneEpsgCode = 25800 + FirstZone;
|
|
||||||
public const int LastZoneEpsgCode = 25800 + LastZone;
|
public Etrs89UtmProjection(int zone)
|
||||||
|
: base($"EPSG:{25800 + zone}", 6378137d, 1d / 298.257222101, zone) // GRS 1980
|
||||||
public Etrs89UtmProjection(int zone)
|
{
|
||||||
: base($"EPSG:{25800 + zone}", 6378137d, 1d / 298.257222101, zone) // GRS 1980
|
if (zone < FirstZone || zone > LastZone)
|
||||||
{
|
{
|
||||||
if (zone < FirstZone || zone > LastZone)
|
throw new ArgumentException($"Invalid ETRS89 UTM zone {zone}.", nameof(zone));
|
||||||
{
|
}
|
||||||
throw new ArgumentException($"Invalid ETRS89 UTM zone {zone}.", nameof(zone));
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
/// <summary>
|
||||||
|
/// NAD83 Universal Transverse Mercator Projection - EPSG:26901 to EPSG:26923.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// NAD83 Universal Transverse Mercator Projection - EPSG:26901 to EPSG:26923.
|
public class Nad83UtmProjection : UtmProjection
|
||||||
/// </summary>
|
{
|
||||||
public class Nad83UtmProjection : UtmProjection
|
public const int FirstZone = 1;
|
||||||
{
|
public const int LastZone = 23;
|
||||||
public const int FirstZone = 1;
|
public const int FirstZoneEpsgCode = 26900 + FirstZone;
|
||||||
public const int LastZone = 23;
|
public const int LastZoneEpsgCode = 26900 + LastZone;
|
||||||
public const int FirstZoneEpsgCode = 26900 + FirstZone;
|
|
||||||
public const int LastZoneEpsgCode = 26900 + LastZone;
|
public Nad83UtmProjection(int zone)
|
||||||
|
: base($"EPSG:{26900 + zone}", 6378137d, 1d / 298.257222101, zone) // GRS 1980
|
||||||
public Nad83UtmProjection(int zone)
|
{
|
||||||
: base($"EPSG:{26900 + zone}", 6378137d, 1d / 298.257222101, zone) // GRS 1980
|
if (zone < FirstZone || zone > LastZone)
|
||||||
{
|
{
|
||||||
if (zone < FirstZone || zone > LastZone)
|
throw new ArgumentException($"Invalid NAD83 UTM zone {zone}.", nameof(zone));
|
||||||
{
|
|
||||||
throw new ArgumentException($"Invalid NAD83 UTM zone {zone}.", nameof(zone));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,152 +6,151 @@ using System.Windows.Media;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the transformation between projected map coordinates in meters
|
||||||
|
/// and view coordinates in pixels.
|
||||||
|
/// </summary>
|
||||||
|
public class ViewTransform
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the transformation between projected map coordinates in meters
|
/// Gets the scaling factor from projected map coordinates to view coordinates,
|
||||||
/// and view coordinates in pixels.
|
/// as pixels per meter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ViewTransform
|
public double Scale { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the rotation angle of the transform matrix.
|
||||||
|
/// </summary>
|
||||||
|
public double Rotation { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the transform matrix from projected map coordinates to view coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Matrix MapToViewMatrix { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the transform matrix from view coordinates to projected map coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Matrix ViewToMapMatrix { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms a Point in projected map coordinates to a Point in view coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Point MapToView(Point point) => MapToViewMatrix.Transform(point);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms a Point in view coordinates to a Point in projected map coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Point ViewToMap(Point point) => ViewToMapMatrix.Transform(point);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an axis-aligned bounding box in projected map coordinates that contains
|
||||||
|
/// a rectangle in view coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Rect ViewToMapBounds(Rect rect) => TransformBounds(ViewToMapMatrix, rect.X, rect.Y, rect.Width, rect.Height);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a ViewTransform from a map center point in projected coordinates,
|
||||||
|
/// a view conter point, a scaling factor from projected coordinates to view coordinates
|
||||||
|
/// and a rotation angle in degrees.
|
||||||
|
/// </summary>
|
||||||
|
public void SetTransform(Point mapCenter, Point viewCenter, double scale, double rotation)
|
||||||
{
|
{
|
||||||
/// <summary>
|
Scale = scale;
|
||||||
/// Gets the scaling factor from projected map coordinates to view coordinates,
|
Rotation = (rotation % 360d + 540d) % 360d - 180d;
|
||||||
/// as pixels per meter.
|
|
||||||
/// </summary>
|
|
||||||
public double Scale { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
var transform = new Matrix(scale, 0d, 0d, -scale, -scale * mapCenter.X, scale * mapCenter.Y);
|
||||||
/// Gets the rotation angle of the transform matrix.
|
transform.Rotate(Rotation);
|
||||||
/// </summary>
|
transform.Translate(viewCenter.X, viewCenter.Y);
|
||||||
public double Rotation { get; private set; }
|
MapToViewMatrix = transform;
|
||||||
|
|
||||||
/// <summary>
|
transform.Invert();
|
||||||
/// Gets the transform matrix from projected map coordinates to view coordinates.
|
ViewToMapMatrix = transform;
|
||||||
/// </summary>
|
}
|
||||||
public Matrix MapToViewMatrix { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the transform matrix from view coordinates to projected map coordinates.
|
/// Gets the transform Matrix for the RenderTranform of a MapTileLayer or WmtsTileMatrixLayer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Matrix ViewToMapMatrix { get; private set; }
|
public Matrix GetTileLayerTransform(double tileMatrixScale, Point tileMatrixTopLeft, Point tileMatrixOrigin)
|
||||||
|
{
|
||||||
|
var scale = Scale / tileMatrixScale;
|
||||||
|
var transform = new Matrix(scale, 0d, 0d, scale, 0d, 0d);
|
||||||
|
transform.Rotate(Rotation);
|
||||||
|
|
||||||
/// <summary>
|
// Tile matrix origin in map coordinates.
|
||||||
/// Transforms a Point in projected map coordinates to a Point in view coordinates.
|
//
|
||||||
/// </summary>
|
var mapOrigin = new Point(
|
||||||
public Point MapToView(Point point) => MapToViewMatrix.Transform(point);
|
tileMatrixTopLeft.X + tileMatrixOrigin.X / tileMatrixScale,
|
||||||
|
tileMatrixTopLeft.Y - tileMatrixOrigin.Y / tileMatrixScale);
|
||||||
|
|
||||||
/// <summary>
|
// Tile matrix origin in view coordinates.
|
||||||
/// Transforms a Point in view coordinates to a Point in projected map coordinates.
|
//
|
||||||
/// </summary>
|
var viewOrigin = MapToViewMatrix.Transform(mapOrigin);
|
||||||
public Point ViewToMap(Point point) => ViewToMapMatrix.Transform(point);
|
transform.Translate(viewOrigin.X, viewOrigin.Y);
|
||||||
|
|
||||||
/// <summary>
|
return transform;
|
||||||
/// Gets an axis-aligned bounding box in projected map coordinates that contains
|
}
|
||||||
/// a rectangle in view coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public Rect ViewToMapBounds(Rect rect) => TransformBounds(ViewToMapMatrix, rect.X, rect.Y, rect.Width, rect.Height);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a ViewTransform from a map center point in projected coordinates,
|
/// Gets the pixel bounds of a tile matrix.
|
||||||
/// a view conter point, a scaling factor from projected coordinates to view coordinates
|
/// </summary>
|
||||||
/// and a rotation angle in degrees.
|
public Rect GetTileMatrixBounds(double tileMatrixScale, Point tileMatrixTopLeft, double viewWidth, double viewHeight)
|
||||||
/// </summary>
|
{
|
||||||
public void SetTransform(Point mapCenter, Point viewCenter, double scale, double rotation)
|
var scale = tileMatrixScale / Scale;
|
||||||
|
var transform = new Matrix(scale, 0d, 0d, scale, 0d, 0d);
|
||||||
|
transform.Rotate(-Rotation);
|
||||||
|
|
||||||
|
// View origin in map coordinates.
|
||||||
|
//
|
||||||
|
var origin = ViewToMapMatrix.Transform(new Point());
|
||||||
|
|
||||||
|
// Translation from origin to tile matrix origin in pixels.
|
||||||
|
//
|
||||||
|
transform.Translate(
|
||||||
|
tileMatrixScale * (origin.X - tileMatrixTopLeft.X),
|
||||||
|
tileMatrixScale * (tileMatrixTopLeft.Y - origin.Y));
|
||||||
|
|
||||||
|
// Transform view bounds to tile pixel bounds.
|
||||||
|
//
|
||||||
|
return TransformBounds(transform, 0d, 0d, viewWidth, viewHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rect TransformBounds(Matrix transform, double x, double y, double width, double height)
|
||||||
|
{
|
||||||
|
if (transform.M12 == 0d && transform.M21 == 0d)
|
||||||
{
|
{
|
||||||
Scale = scale;
|
x = x * transform.M11 + transform.OffsetX;
|
||||||
Rotation = (rotation % 360d + 540d) % 360d - 180d;
|
y = y * transform.M22 + transform.OffsetY;
|
||||||
|
width *= transform.M11;
|
||||||
|
height *= transform.M22;
|
||||||
|
|
||||||
var transform = new Matrix(scale, 0d, 0d, -scale, -scale * mapCenter.X, scale * mapCenter.Y);
|
if (width < 0d)
|
||||||
transform.Rotate(Rotation);
|
|
||||||
transform.Translate(viewCenter.X, viewCenter.Y);
|
|
||||||
MapToViewMatrix = transform;
|
|
||||||
|
|
||||||
transform.Invert();
|
|
||||||
ViewToMapMatrix = transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the transform Matrix for the RenderTranform of a MapTileLayer or WmtsTileMatrixLayer.
|
|
||||||
/// </summary>
|
|
||||||
public Matrix GetTileLayerTransform(double tileMatrixScale, Point tileMatrixTopLeft, Point tileMatrixOrigin)
|
|
||||||
{
|
|
||||||
var scale = Scale / tileMatrixScale;
|
|
||||||
var transform = new Matrix(scale, 0d, 0d, scale, 0d, 0d);
|
|
||||||
transform.Rotate(Rotation);
|
|
||||||
|
|
||||||
// Tile matrix origin in map coordinates.
|
|
||||||
//
|
|
||||||
var mapOrigin = new Point(
|
|
||||||
tileMatrixTopLeft.X + tileMatrixOrigin.X / tileMatrixScale,
|
|
||||||
tileMatrixTopLeft.Y - tileMatrixOrigin.Y / tileMatrixScale);
|
|
||||||
|
|
||||||
// Tile matrix origin in view coordinates.
|
|
||||||
//
|
|
||||||
var viewOrigin = MapToViewMatrix.Transform(mapOrigin);
|
|
||||||
transform.Translate(viewOrigin.X, viewOrigin.Y);
|
|
||||||
|
|
||||||
return transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the pixel bounds of a tile matrix.
|
|
||||||
/// </summary>
|
|
||||||
public Rect GetTileMatrixBounds(double tileMatrixScale, Point tileMatrixTopLeft, double viewWidth, double viewHeight)
|
|
||||||
{
|
|
||||||
var scale = tileMatrixScale / Scale;
|
|
||||||
var transform = new Matrix(scale, 0d, 0d, scale, 0d, 0d);
|
|
||||||
transform.Rotate(-Rotation);
|
|
||||||
|
|
||||||
// View origin in map coordinates.
|
|
||||||
//
|
|
||||||
var origin = ViewToMapMatrix.Transform(new Point());
|
|
||||||
|
|
||||||
// Translation from origin to tile matrix origin in pixels.
|
|
||||||
//
|
|
||||||
transform.Translate(
|
|
||||||
tileMatrixScale * (origin.X - tileMatrixTopLeft.X),
|
|
||||||
tileMatrixScale * (tileMatrixTopLeft.Y - origin.Y));
|
|
||||||
|
|
||||||
// Transform view bounds to tile pixel bounds.
|
|
||||||
//
|
|
||||||
return TransformBounds(transform, 0d, 0d, viewWidth, viewHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Rect TransformBounds(Matrix transform, double x, double y, double width, double height)
|
|
||||||
{
|
|
||||||
if (transform.M12 == 0d && transform.M21 == 0d)
|
|
||||||
{
|
{
|
||||||
x = x * transform.M11 + transform.OffsetX;
|
width = -width;
|
||||||
y = y * transform.M22 + transform.OffsetY;
|
x -= width;
|
||||||
width *= transform.M11;
|
|
||||||
height *= transform.M22;
|
|
||||||
|
|
||||||
if (width < 0d)
|
|
||||||
{
|
|
||||||
width = -width;
|
|
||||||
x -= width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (height < 0d)
|
|
||||||
{
|
|
||||||
height = -height;
|
|
||||||
y -= height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var p1 = transform.Transform(new Point(x, y));
|
|
||||||
var p2 = transform.Transform(new Point(x, y + height));
|
|
||||||
var p3 = transform.Transform(new Point(x + width, y));
|
|
||||||
var p4 = transform.Transform(new Point(x + width, y + height));
|
|
||||||
|
|
||||||
x = Math.Min(p1.X, Math.Min(p2.X, Math.Min(p3.X, p4.X)));
|
|
||||||
y = Math.Min(p1.Y, Math.Min(p2.Y, Math.Min(p3.Y, p4.Y)));
|
|
||||||
width = Math.Max(p1.X, Math.Max(p2.X, Math.Max(p3.X, p4.X))) - x;
|
|
||||||
height = Math.Max(p1.Y, Math.Max(p2.Y, Math.Max(p3.Y, p4.Y))) - y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Rect(x, y, width, height);
|
if (height < 0d)
|
||||||
|
{
|
||||||
|
height = -height;
|
||||||
|
y -= height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var p1 = transform.Transform(new Point(x, y));
|
||||||
|
var p2 = transform.Transform(new Point(x, y + height));
|
||||||
|
var p3 = transform.Transform(new Point(x + width, y));
|
||||||
|
var p4 = transform.Transform(new Point(x + width, y + height));
|
||||||
|
|
||||||
|
x = Math.Min(p1.X, Math.Min(p2.X, Math.Min(p3.X, p4.X)));
|
||||||
|
y = Math.Min(p1.Y, Math.Min(p2.Y, Math.Min(p3.Y, p4.Y)));
|
||||||
|
width = Math.Max(p1.X, Math.Max(p2.X, Math.Max(p3.X, p4.X))) - x;
|
||||||
|
height = Math.Max(p1.Y, Math.Max(p2.Y, Math.Max(p3.Y, p4.Y))) - y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Rect(x, y, width, height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
{
|
|
||||||
public class ViewportChangedEventArgs(bool projectionChanged = false, bool transformCenterChanged = false) : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates that the map projection has changed. Used to control when
|
|
||||||
/// a MapTileLayer or a MapImageLayer should be updated immediately,
|
|
||||||
/// or MapPath Data in projected map coordinates should be recalculated.
|
|
||||||
/// </summary>
|
|
||||||
public bool ProjectionChanged => projectionChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
public class ViewportChangedEventArgs(bool projectionChanged = false, bool transformCenterChanged = false) : EventArgs
|
||||||
/// Indicates that the view transform center has moved across 180° longitude.
|
{
|
||||||
/// Used to control when a MapTileLayer should be updated immediately.
|
/// <summary>
|
||||||
/// </summary>
|
/// Indicates that the map projection has changed. Used to control when
|
||||||
public bool TransformCenterChanged => transformCenterChanged;
|
/// a MapTileLayer or a MapImageLayer should be updated immediately,
|
||||||
}
|
/// or MapPath Data in projected map coordinates should be recalculated.
|
||||||
|
/// </summary>
|
||||||
|
public bool ProjectionChanged => projectionChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that the view transform center has moved across 180° longitude.
|
||||||
|
/// Used to control when a MapTileLayer should be updated immediately.
|
||||||
|
/// </summary>
|
||||||
|
public bool TransformCenterChanged => transformCenterChanged;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,66 +6,65 @@ using System.Windows.Media;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spherical Mercator Projection - EPSG:3857.
|
||||||
|
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.41-44.
|
||||||
|
/// </summary>
|
||||||
|
public class WebMercatorProjection : MapProjection
|
||||||
{
|
{
|
||||||
/// <summary>
|
public const string DefaultCrsId = "EPSG:3857";
|
||||||
/// Spherical Mercator Projection - EPSG:3857.
|
|
||||||
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.41-44.
|
public WebMercatorProjection() // parameterless constructor for XAML
|
||||||
/// </summary>
|
: this(DefaultCrsId)
|
||||||
public class WebMercatorProjection : MapProjection
|
|
||||||
{
|
{
|
||||||
public const string DefaultCrsId = "EPSG:3857";
|
}
|
||||||
|
|
||||||
public WebMercatorProjection() // parameterless constructor for XAML
|
public WebMercatorProjection(string crsId)
|
||||||
: this(DefaultCrsId)
|
{
|
||||||
|
IsNormalCylindrical = true;
|
||||||
|
CrsId = crsId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
var k = 1d / Math.Cos(latitude * Math.PI / 180d); // p.44 (7-3)
|
||||||
|
|
||||||
|
return new Matrix(k, 0d, 0d, k, 0d, 0d);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Point LocationToMap(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
return new Point(
|
||||||
|
EquatorialRadius * Math.PI / 180d * longitude,
|
||||||
|
EquatorialRadius * Math.PI / 180d * LatitudeToY(latitude));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Location MapToLocation(double x, double y)
|
||||||
|
{
|
||||||
|
return new Location(YToLatitude(
|
||||||
|
y / EquatorialRadius * 180d / Math.PI),
|
||||||
|
x / EquatorialRadius * 180d / Math.PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double LatitudeToY(double latitude)
|
||||||
|
{
|
||||||
|
if (latitude <= -90d)
|
||||||
{
|
{
|
||||||
|
return double.NegativeInfinity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebMercatorProjection(string crsId)
|
if (latitude >= 90d)
|
||||||
{
|
{
|
||||||
IsNormalCylindrical = true;
|
return double.PositiveInfinity;
|
||||||
CrsId = crsId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
return Math.Log(Math.Tan((latitude + 90d) * Math.PI / 360d)) * 180d / Math.PI;
|
||||||
{
|
}
|
||||||
var k = 1d / Math.Cos(latitude * Math.PI / 180d); // p.44 (7-3)
|
|
||||||
|
|
||||||
return new Matrix(k, 0d, 0d, k, 0d, 0d);
|
public static double YToLatitude(double y)
|
||||||
}
|
{
|
||||||
|
return 90d - Math.Atan(Math.Exp(-y * Math.PI / 180d)) * 360d / Math.PI;
|
||||||
public override Point LocationToMap(double latitude, double longitude)
|
|
||||||
{
|
|
||||||
return new Point(
|
|
||||||
EquatorialRadius * Math.PI / 180d * longitude,
|
|
||||||
EquatorialRadius * Math.PI / 180d * LatitudeToY(latitude));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Location MapToLocation(double x, double y)
|
|
||||||
{
|
|
||||||
return new Location(YToLatitude(
|
|
||||||
y / EquatorialRadius * 180d / Math.PI),
|
|
||||||
x / EquatorialRadius * 180d / Math.PI);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static double LatitudeToY(double latitude)
|
|
||||||
{
|
|
||||||
if (latitude <= -90d)
|
|
||||||
{
|
|
||||||
return double.NegativeInfinity;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latitude >= 90d)
|
|
||||||
{
|
|
||||||
return double.PositiveInfinity;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.Log(Math.Tan((latitude + 90d) * Math.PI / 360d)) * 180d / Math.PI;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static double YToLatitude(double y)
|
|
||||||
{
|
|
||||||
return 90d - Math.Atan(Math.Exp(-y * Math.PI / 180d)) * 360d / Math.PI;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,336 +20,335 @@ using Avalonia.Interactivity;
|
||||||
using ImageSource = Avalonia.Media.IImage;
|
using ImageSource = Avalonia.Media.IImage;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Displays a single map image from a Web Map Service (WMS).
|
||||||
|
/// </summary>
|
||||||
|
public partial class WmsImageLayer : MapImageLayer
|
||||||
{
|
{
|
||||||
|
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(WmsImageLayer));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ServiceUriProperty =
|
||||||
|
DependencyPropertyHelper.Register<WmsImageLayer, Uri>(nameof(ServiceUri), null,
|
||||||
|
async (layer, oldValue, newValue) => await layer.UpdateImageAsync());
|
||||||
|
|
||||||
|
public static readonly DependencyProperty RequestStylesProperty =
|
||||||
|
DependencyPropertyHelper.Register<WmsImageLayer, string>(nameof(RequestStyles), "",
|
||||||
|
async (layer, oldValue, newValue) => await layer.UpdateImageAsync());
|
||||||
|
|
||||||
|
public static readonly DependencyProperty RequestLayersProperty =
|
||||||
|
DependencyPropertyHelper.Register<WmsImageLayer, string>(nameof(RequestLayers), null,
|
||||||
|
async (layer, oldValue, newValue) => await layer.UpdateImageAsync());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Displays a single map image from a Web Map Service (WMS).
|
/// The base request URL.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class WmsImageLayer : MapImageLayer
|
public Uri ServiceUri
|
||||||
{
|
{
|
||||||
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(WmsImageLayer));
|
get => (Uri)GetValue(ServiceUriProperty);
|
||||||
|
set => SetValue(ServiceUriProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty ServiceUriProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<WmsImageLayer, Uri>(nameof(ServiceUri), null,
|
/// Comma-separated sequence of requested WMS Styles. Default is an empty string.
|
||||||
async (layer, oldValue, newValue) => await layer.UpdateImageAsync());
|
/// </summary>
|
||||||
|
public string RequestStyles
|
||||||
|
{
|
||||||
|
get => (string)GetValue(RequestStylesProperty);
|
||||||
|
set => SetValue(RequestStylesProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty RequestStylesProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<WmsImageLayer, string>(nameof(RequestStyles), "",
|
/// Comma-separated sequence of WMS Layer names to be displayed. If not set, the default Layer is displayed.
|
||||||
async (layer, oldValue, newValue) => await layer.UpdateImageAsync());
|
/// </summary>
|
||||||
|
public string RequestLayers
|
||||||
|
{
|
||||||
|
get => (string)GetValue(RequestLayersProperty);
|
||||||
|
set => SetValue(RequestLayersProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty RequestLayersProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<WmsImageLayer, string>(nameof(RequestLayers), null,
|
/// Gets a collection of all Layer names available in a WMS.
|
||||||
async (layer, oldValue, newValue) => await layer.UpdateImageAsync());
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<string> AvailableLayers { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base request URL.
|
/// Gets a collection of all CRSs supported by a WMS.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Uri ServiceUri
|
public IReadOnlyCollection<string> SupportedCrsIds { get; private set; }
|
||||||
|
|
||||||
|
private bool HasLayer =>
|
||||||
|
RequestLayers != null ||
|
||||||
|
AvailableLayers?.Count > 0 ||
|
||||||
|
ServiceUri.Query?.IndexOf("LAYERS=", StringComparison.OrdinalIgnoreCase) > 0;
|
||||||
|
|
||||||
|
public WmsImageLayer()
|
||||||
|
{
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Loaded -= OnLoaded;
|
||||||
|
|
||||||
|
if (ServiceUri != null && !HasLayer)
|
||||||
{
|
{
|
||||||
get => (Uri)GetValue(ServiceUriProperty);
|
await InitializeAsync();
|
||||||
set => SetValue(ServiceUriProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
if (AvailableLayers != null && AvailableLayers.Count > 0)
|
||||||
/// Comma-separated sequence of requested WMS Styles. Default is an empty string.
|
|
||||||
/// </summary>
|
|
||||||
public string RequestStyles
|
|
||||||
{
|
|
||||||
get => (string)GetValue(RequestStylesProperty);
|
|
||||||
set => SetValue(RequestStylesProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Comma-separated sequence of WMS Layer names to be displayed. If not set, the default Layer is displayed.
|
|
||||||
/// </summary>
|
|
||||||
public string RequestLayers
|
|
||||||
{
|
|
||||||
get => (string)GetValue(RequestLayersProperty);
|
|
||||||
set => SetValue(RequestLayersProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a collection of all Layer names available in a WMS.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyCollection<string> AvailableLayers { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a collection of all CRSs supported by a WMS.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyCollection<string> SupportedCrsIds { get; private set; }
|
|
||||||
|
|
||||||
private bool HasLayer =>
|
|
||||||
RequestLayers != null ||
|
|
||||||
AvailableLayers?.Count > 0 ||
|
|
||||||
ServiceUri.Query?.IndexOf("LAYERS=", StringComparison.OrdinalIgnoreCase) > 0;
|
|
||||||
|
|
||||||
public WmsImageLayer()
|
|
||||||
{
|
|
||||||
Loaded += OnLoaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnLoaded(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
Loaded -= OnLoaded;
|
|
||||||
|
|
||||||
if (ServiceUri != null && !HasLayer)
|
|
||||||
{
|
{
|
||||||
await InitializeAsync();
|
await UpdateImageAsync();
|
||||||
|
|
||||||
if (AvailableLayers != null && AvailableLayers.Count > 0)
|
|
||||||
{
|
|
||||||
await UpdateImageAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Initializes the AvailableLayers and SupportedCrsIds properties.
|
/// <summary>
|
||||||
/// Calling this method is only necessary when no layer name is known in advance.
|
/// Initializes the AvailableLayers and SupportedCrsIds properties.
|
||||||
/// It is called internally in a Loaded event handler when the RequestLayers and AvailableLayers
|
/// Calling this method is only necessary when no layer name is known in advance.
|
||||||
/// properties are null and the ServiceUri.Query part does not contain a LAYERS parameter.
|
/// It is called internally in a Loaded event handler when the RequestLayers and AvailableLayers
|
||||||
/// </summary>
|
/// properties are null and the ServiceUri.Query part does not contain a LAYERS parameter.
|
||||||
public async Task InitializeAsync()
|
/// </summary>
|
||||||
{
|
public async Task InitializeAsync()
|
||||||
var capabilities = await GetCapabilitiesAsync();
|
{
|
||||||
|
var capabilities = await GetCapabilitiesAsync();
|
||||||
if (capabilities != null)
|
|
||||||
{
|
if (capabilities != null)
|
||||||
var ns = capabilities.Name.Namespace;
|
{
|
||||||
var capability = capabilities.Element(ns + "Capability");
|
var ns = capabilities.Name.Namespace;
|
||||||
|
var capability = capabilities.Element(ns + "Capability");
|
||||||
AvailableLayers = capability
|
|
||||||
.Descendants(ns + "Layer")
|
AvailableLayers = capability
|
||||||
.Select(e => e.Element(ns + "Name")?.Value)
|
.Descendants(ns + "Layer")
|
||||||
.Where(n => !string.IsNullOrEmpty(n))
|
.Select(e => e.Element(ns + "Name")?.Value)
|
||||||
.ToList();
|
.Where(n => !string.IsNullOrEmpty(n))
|
||||||
|
.ToList();
|
||||||
SupportedCrsIds = capability
|
|
||||||
.Descendants(ns + "Layer")
|
SupportedCrsIds = capability
|
||||||
.Descendants(ns + "CRS")
|
.Descendants(ns + "Layer")
|
||||||
.Select(e => e.Value)
|
.Descendants(ns + "CRS")
|
||||||
.ToList();
|
.Select(e => e.Value)
|
||||||
}
|
.ToList();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Loads an XElement from the URL returned by GetCapabilitiesRequestUri().
|
/// <summary>
|
||||||
/// </summary>
|
/// Loads an XElement from the URL returned by GetCapabilitiesRequestUri().
|
||||||
public async Task<XElement> GetCapabilitiesAsync()
|
/// </summary>
|
||||||
{
|
public async Task<XElement> GetCapabilitiesAsync()
|
||||||
XElement element = null;
|
{
|
||||||
|
XElement element = null;
|
||||||
if (ServiceUri != null)
|
|
||||||
{
|
if (ServiceUri != null)
|
||||||
var uri = GetCapabilitiesRequestUri();
|
{
|
||||||
|
var uri = GetCapabilitiesRequestUri();
|
||||||
try
|
|
||||||
{
|
try
|
||||||
using var stream = await ImageLoader.HttpClient.GetStreamAsync(uri);
|
{
|
||||||
|
using var stream = await ImageLoader.HttpClient.GetStreamAsync(uri);
|
||||||
element = await XDocument.LoadRootElementAsync(stream);
|
|
||||||
}
|
element = await XDocument.LoadRootElementAsync(stream);
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
Logger?.LogError(ex, "Failed reading capabilities from {uri}", uri);
|
{
|
||||||
}
|
Logger?.LogError(ex, "Failed reading capabilities from {uri}", uri);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return element;
|
|
||||||
}
|
return element;
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Gets a response string from the URL returned by GetFeatureInfoRequestUri().
|
/// <summary>
|
||||||
/// </summary>
|
/// Gets a response string from the URL returned by GetFeatureInfoRequestUri().
|
||||||
public async Task<string> GetFeatureInfoAsync(Point position, string format = "text/plain")
|
/// </summary>
|
||||||
{
|
public async Task<string> GetFeatureInfoAsync(Point position, string format = "text/plain")
|
||||||
string response = null;
|
{
|
||||||
|
string response = null;
|
||||||
if (ServiceUri != null && HasLayer &&
|
|
||||||
ParentMap != null && ParentMap.InsideViewBounds(position) &&
|
if (ServiceUri != null && HasLayer &&
|
||||||
(SupportedCrsIds == null || SupportedCrsIds.Contains(ParentMap.MapProjection.CrsId)))
|
ParentMap != null && ParentMap.InsideViewBounds(position) &&
|
||||||
{
|
(SupportedCrsIds == null || SupportedCrsIds.Contains(ParentMap.MapProjection.CrsId)))
|
||||||
var uri = GetFeatureInfoRequestUri(position, format);
|
{
|
||||||
|
var uri = GetFeatureInfoRequestUri(position, format);
|
||||||
try
|
|
||||||
{
|
try
|
||||||
response = await ImageLoader.HttpClient.GetStringAsync(uri);
|
{
|
||||||
}
|
response = await ImageLoader.HttpClient.GetStringAsync(uri);
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
Logger?.LogError(ex, "Failed reading feature info from {uri}", uri);
|
{
|
||||||
}
|
Logger?.LogError(ex, "Failed reading feature info from {uri}", uri);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return response;
|
|
||||||
}
|
return response;
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Loads an ImageSource from the URL returned by GetMapRequestUri().
|
/// <summary>
|
||||||
/// </summary>
|
/// Loads an ImageSource from the URL returned by GetMapRequestUri().
|
||||||
protected override async Task<ImageSource> GetImageAsync(Rect bbox, IProgress<double> progress)
|
/// </summary>
|
||||||
{
|
protected override async Task<ImageSource> GetImageAsync(Rect bbox, IProgress<double> progress)
|
||||||
ImageSource image = null;
|
{
|
||||||
|
ImageSource image = null;
|
||||||
if (ServiceUri != null && HasLayer &&
|
|
||||||
(SupportedCrsIds == null || SupportedCrsIds.Contains(ParentMap.MapProjection.CrsId)))
|
if (ServiceUri != null && HasLayer &&
|
||||||
{
|
(SupportedCrsIds == null || SupportedCrsIds.Contains(ParentMap.MapProjection.CrsId)))
|
||||||
var xMin = -180d * MapProjection.Wgs84MeterPerDegree;
|
{
|
||||||
var xMax = 180d * MapProjection.Wgs84MeterPerDegree;
|
var xMin = -180d * MapProjection.Wgs84MeterPerDegree;
|
||||||
|
var xMax = 180d * MapProjection.Wgs84MeterPerDegree;
|
||||||
if (!ParentMap.MapProjection.IsNormalCylindrical ||
|
|
||||||
bbox.X >= xMin && bbox.X + bbox.Width <= xMax)
|
if (!ParentMap.MapProjection.IsNormalCylindrical ||
|
||||||
{
|
bbox.X >= xMin && bbox.X + bbox.Width <= xMax)
|
||||||
var uri = GetMapRequestUri(bbox);
|
{
|
||||||
|
var uri = GetMapRequestUri(bbox);
|
||||||
image = await ImageLoader.LoadImageAsync(uri, progress);
|
|
||||||
}
|
image = await ImageLoader.LoadImageAsync(uri, progress);
|
||||||
else
|
}
|
||||||
{
|
else
|
||||||
var x = bbox.X;
|
{
|
||||||
|
var x = bbox.X;
|
||||||
if (x < xMin)
|
|
||||||
{
|
if (x < xMin)
|
||||||
x += xMax - xMin;
|
{
|
||||||
}
|
x += xMax - xMin;
|
||||||
|
}
|
||||||
var width1 = Math.Floor(xMax * 1e3) / 1e3 - x; // round down xMax to avoid gap between images
|
|
||||||
var width2 = bbox.Width - width1;
|
var width1 = Math.Floor(xMax * 1e3) / 1e3 - x; // round down xMax to avoid gap between images
|
||||||
var bbox1 = new Rect(x, bbox.Y, width1, bbox.Height);
|
var width2 = bbox.Width - width1;
|
||||||
var bbox2 = new Rect(xMin, bbox.Y, width2, bbox.Height);
|
var bbox1 = new Rect(x, bbox.Y, width1, bbox.Height);
|
||||||
var uri1 = GetMapRequestUri(bbox1);
|
var bbox2 = new Rect(xMin, bbox.Y, width2, bbox.Height);
|
||||||
var uri2 = GetMapRequestUri(bbox2);
|
var uri1 = GetMapRequestUri(bbox1);
|
||||||
|
var uri2 = GetMapRequestUri(bbox2);
|
||||||
image = await ImageLoader.LoadMergedImageAsync(uri1, uri2, progress);
|
|
||||||
}
|
image = await ImageLoader.LoadMergedImageAsync(uri1, uri2, progress);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return image;
|
|
||||||
}
|
return image;
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Returns a GetCapabilities request URL string.
|
/// <summary>
|
||||||
/// </summary>
|
/// Returns a GetCapabilities request URL string.
|
||||||
protected virtual Uri GetCapabilitiesRequestUri()
|
/// </summary>
|
||||||
{
|
protected virtual Uri GetCapabilitiesRequestUri()
|
||||||
return GetRequestUri(new Dictionary<string, string>
|
{
|
||||||
{
|
return GetRequestUri(new Dictionary<string, string>
|
||||||
{ "SERVICE", "WMS" },
|
{
|
||||||
{ "VERSION", "1.3.0" },
|
{ "SERVICE", "WMS" },
|
||||||
{ "REQUEST", "GetCapabilities" }
|
{ "VERSION", "1.3.0" },
|
||||||
});
|
{ "REQUEST", "GetCapabilities" }
|
||||||
}
|
});
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Returns a GetMap request URL string.
|
/// <summary>
|
||||||
/// </summary>
|
/// Returns a GetMap request URL string.
|
||||||
protected virtual Uri GetMapRequestUri(Rect bbox)
|
/// </summary>
|
||||||
{
|
protected virtual Uri GetMapRequestUri(Rect bbox)
|
||||||
var width = ParentMap.ViewTransform.Scale * bbox.Width;
|
{
|
||||||
var height = ParentMap.ViewTransform.Scale * bbox.Height;
|
var width = ParentMap.ViewTransform.Scale * bbox.Width;
|
||||||
|
var height = ParentMap.ViewTransform.Scale * bbox.Height;
|
||||||
return GetRequestUri(new Dictionary<string, string>
|
|
||||||
{
|
return GetRequestUri(new Dictionary<string, string>
|
||||||
{ "SERVICE", "WMS" },
|
{
|
||||||
{ "VERSION", "1.3.0" },
|
{ "SERVICE", "WMS" },
|
||||||
{ "REQUEST", "GetMap" },
|
{ "VERSION", "1.3.0" },
|
||||||
{ "LAYERS", RequestLayers ?? AvailableLayers?.FirstOrDefault() ?? "" },
|
{ "REQUEST", "GetMap" },
|
||||||
{ "STYLES", RequestStyles ?? "" },
|
{ "LAYERS", RequestLayers ?? AvailableLayers?.FirstOrDefault() ?? "" },
|
||||||
{ "FORMAT", "image/png" },
|
{ "STYLES", RequestStyles ?? "" },
|
||||||
{ "CRS", ParentMap.MapProjection.CrsId },
|
{ "FORMAT", "image/png" },
|
||||||
{ "BBOX", GetBboxValue(bbox) },
|
{ "CRS", ParentMap.MapProjection.CrsId },
|
||||||
{ "WIDTH", Math.Ceiling(width).ToString("F0") },
|
{ "BBOX", GetBboxValue(bbox) },
|
||||||
{ "HEIGHT", Math.Ceiling(height).ToString("F0") }
|
{ "WIDTH", Math.Ceiling(width).ToString("F0") },
|
||||||
});
|
{ "HEIGHT", Math.Ceiling(height).ToString("F0") }
|
||||||
}
|
});
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Returns a GetFeatureInfo request URL string.
|
/// <summary>
|
||||||
/// </summary>
|
/// Returns a GetFeatureInfo request URL string.
|
||||||
protected virtual Uri GetFeatureInfoRequestUri(Point position, string format)
|
/// </summary>
|
||||||
{
|
protected virtual Uri GetFeatureInfoRequestUri(Point position, string format)
|
||||||
var width = ParentMap.ActualWidth;
|
{
|
||||||
var height = ParentMap.ActualHeight;
|
var width = ParentMap.ActualWidth;
|
||||||
var bbox = ParentMap.ViewTransform.ViewToMapBounds(new Rect(0d, 0d, width, height));
|
var height = ParentMap.ActualHeight;
|
||||||
|
var bbox = ParentMap.ViewTransform.ViewToMapBounds(new Rect(0d, 0d, width, height));
|
||||||
if (ParentMap.ViewTransform.Rotation != 0d)
|
|
||||||
{
|
if (ParentMap.ViewTransform.Rotation != 0d)
|
||||||
width = ParentMap.ViewTransform.Scale * bbox.Width;
|
{
|
||||||
height = ParentMap.ViewTransform.Scale * bbox.Height;
|
width = ParentMap.ViewTransform.Scale * bbox.Width;
|
||||||
|
height = ParentMap.ViewTransform.Scale * bbox.Height;
|
||||||
var transform = new Matrix(1d, 0d, 0d, 1d, -ParentMap.ActualWidth / 2d, -ParentMap.ActualHeight / 2d);
|
|
||||||
transform.Rotate(-ParentMap.ViewTransform.Rotation);
|
var transform = new Matrix(1d, 0d, 0d, 1d, -ParentMap.ActualWidth / 2d, -ParentMap.ActualHeight / 2d);
|
||||||
transform.Translate(width / 2d, height / 2d);
|
transform.Rotate(-ParentMap.ViewTransform.Rotation);
|
||||||
|
transform.Translate(width / 2d, height / 2d);
|
||||||
position = transform.Transform(position);
|
|
||||||
}
|
position = transform.Transform(position);
|
||||||
|
}
|
||||||
var queryParameters = new Dictionary<string, string>
|
|
||||||
{
|
var queryParameters = new Dictionary<string, string>
|
||||||
{ "SERVICE", "WMS" },
|
{
|
||||||
{ "VERSION", "1.3.0" },
|
{ "SERVICE", "WMS" },
|
||||||
{ "REQUEST", "GetFeatureInfo" },
|
{ "VERSION", "1.3.0" },
|
||||||
{ "LAYERS", RequestLayers ?? AvailableLayers?.FirstOrDefault() ?? "" },
|
{ "REQUEST", "GetFeatureInfo" },
|
||||||
{ "STYLES", RequestStyles ?? "" },
|
{ "LAYERS", RequestLayers ?? AvailableLayers?.FirstOrDefault() ?? "" },
|
||||||
{ "INFO_FORMAT", format },
|
{ "STYLES", RequestStyles ?? "" },
|
||||||
{ "CRS", ParentMap.MapProjection.CrsId },
|
{ "INFO_FORMAT", format },
|
||||||
{ "BBOX", GetBboxValue(bbox) },
|
{ "CRS", ParentMap.MapProjection.CrsId },
|
||||||
{ "WIDTH", Math.Ceiling(width).ToString("F0") },
|
{ "BBOX", GetBboxValue(bbox) },
|
||||||
{ "HEIGHT", Math.Ceiling(height).ToString("F0") },
|
{ "WIDTH", Math.Ceiling(width).ToString("F0") },
|
||||||
{ "I", position.X.ToString("F0") },
|
{ "HEIGHT", Math.Ceiling(height).ToString("F0") },
|
||||||
{ "J", position.Y.ToString("F0") }
|
{ "I", position.X.ToString("F0") },
|
||||||
};
|
{ "J", position.Y.ToString("F0") }
|
||||||
|
};
|
||||||
// GetRequestUri may modify queryParameters["LAYERS"].
|
|
||||||
//
|
// GetRequestUri may modify queryParameters["LAYERS"].
|
||||||
var uriBuilder = new UriBuilder(GetRequestUri(queryParameters));
|
//
|
||||||
|
var uriBuilder = new UriBuilder(GetRequestUri(queryParameters));
|
||||||
uriBuilder.Query += "&QUERY_LAYERS=" + queryParameters["LAYERS"];
|
|
||||||
|
uriBuilder.Query += "&QUERY_LAYERS=" + queryParameters["LAYERS"];
|
||||||
return uriBuilder.Uri;
|
|
||||||
}
|
return uriBuilder.Uri;
|
||||||
|
}
|
||||||
protected virtual Uri GetRequestUri(IDictionary<string, string> queryParameters)
|
|
||||||
{
|
protected virtual Uri GetRequestUri(IDictionary<string, string> queryParameters)
|
||||||
var query = ServiceUri.Query;
|
{
|
||||||
|
var query = ServiceUri.Query;
|
||||||
if (!string.IsNullOrEmpty(query))
|
|
||||||
{
|
if (!string.IsNullOrEmpty(query))
|
||||||
// Parameters from ServiceUri.Query take higher precedence than queryParameters.
|
{
|
||||||
//
|
// Parameters from ServiceUri.Query take higher precedence than queryParameters.
|
||||||
foreach (var param in query.Substring(1).Split('&'))
|
//
|
||||||
{
|
foreach (var param in query.Substring(1).Split('&'))
|
||||||
var pair = param.Split('=');
|
{
|
||||||
queryParameters[pair[0]] = pair.Length > 1 ? pair[1] : "";
|
var pair = param.Split('=');
|
||||||
}
|
queryParameters[pair[0]] = pair.Length > 1 ? pair[1] : "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
query = string.Join("&", queryParameters.Select(kv => kv.Key + "=" + kv.Value));
|
|
||||||
|
query = string.Join("&", queryParameters.Select(kv => kv.Key + "=" + kv.Value));
|
||||||
return new Uri(ServiceUri.GetLeftPart(UriPartial.Path) + "?" + query);
|
|
||||||
}
|
return new Uri(ServiceUri.GetLeftPart(UriPartial.Path) + "?" + query);
|
||||||
|
}
|
||||||
protected virtual string GetBboxValue(Rect bbox)
|
|
||||||
{
|
protected virtual string GetBboxValue(Rect bbox)
|
||||||
var crs = ParentMap.MapProjection.CrsId;
|
{
|
||||||
var format = "{0:0.###},{1:0.###},{2:0.###},{3:0.###}";
|
var crs = ParentMap.MapProjection.CrsId;
|
||||||
var x1 = bbox.X;
|
var format = "{0:0.###},{1:0.###},{2:0.###},{3:0.###}";
|
||||||
var y1 = bbox.Y;
|
var x1 = bbox.X;
|
||||||
var x2 = bbox.X + bbox.Width;
|
var y1 = bbox.Y;
|
||||||
var y2 = bbox.Y + bbox.Height;
|
var x2 = bbox.X + bbox.Width;
|
||||||
|
var y2 = bbox.Y + bbox.Height;
|
||||||
if (crs == "CRS:84" || crs == "EPSG:4326")
|
|
||||||
{
|
if (crs == "CRS:84" || crs == "EPSG:4326")
|
||||||
format = crs == "CRS:84"
|
{
|
||||||
? "{0:0.########},{1:0.########},{2:0.########},{3:0.########}"
|
format = crs == "CRS:84"
|
||||||
: "{1:0.########},{0:0.########},{3:0.########},{2:0.########}";
|
? "{0:0.########},{1:0.########},{2:0.########},{3:0.########}"
|
||||||
x1 /= MapProjection.Wgs84MeterPerDegree;
|
: "{1:0.########},{0:0.########},{3:0.########},{2:0.########}";
|
||||||
y1 /= MapProjection.Wgs84MeterPerDegree;
|
x1 /= MapProjection.Wgs84MeterPerDegree;
|
||||||
x2 /= MapProjection.Wgs84MeterPerDegree;
|
y1 /= MapProjection.Wgs84MeterPerDegree;
|
||||||
y2 /= MapProjection.Wgs84MeterPerDegree;
|
x2 /= MapProjection.Wgs84MeterPerDegree;
|
||||||
}
|
y2 /= MapProjection.Wgs84MeterPerDegree;
|
||||||
|
}
|
||||||
return string.Format(CultureInfo.InvariantCulture, format, x1, y1, x2, y2);
|
|
||||||
}
|
return string.Format(CultureInfo.InvariantCulture, format, x1, y1, x2, y2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,293 +11,292 @@ using System.Windows;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// See https://www.ogc.org/standards/wmts, 07-057r7_Web_Map_Tile_Service_Standard.pdf.
|
||||||
|
/// </summary>
|
||||||
|
public class WmtsCapabilities
|
||||||
{
|
{
|
||||||
/// <summary>
|
private static readonly XNamespace ows = "http://www.opengis.net/ows/1.1";
|
||||||
/// See https://www.ogc.org/standards/wmts, 07-057r7_Web_Map_Tile_Service_Standard.pdf.
|
private static readonly XNamespace wmts = "http://www.opengis.net/wmts/1.0";
|
||||||
/// </summary>
|
private static readonly XNamespace xlink = "http://www.w3.org/1999/xlink";
|
||||||
public class WmtsCapabilities
|
|
||||||
|
public string Layer { get; private set; }
|
||||||
|
public List<WmtsTileMatrixSet> TileMatrixSets { get; private set; }
|
||||||
|
|
||||||
|
public static async Task<WmtsCapabilities> ReadCapabilitiesAsync(Uri uri, string layer)
|
||||||
{
|
{
|
||||||
private static readonly XNamespace ows = "http://www.opengis.net/ows/1.1";
|
Stream xmlStream;
|
||||||
private static readonly XNamespace wmts = "http://www.opengis.net/wmts/1.0";
|
string defaultUri = null;
|
||||||
private static readonly XNamespace xlink = "http://www.w3.org/1999/xlink";
|
|
||||||
|
|
||||||
public string Layer { get; private set; }
|
if (!uri.IsAbsoluteUri)
|
||||||
public List<WmtsTileMatrixSet> TileMatrixSets { get; private set; }
|
|
||||||
|
|
||||||
public static async Task<WmtsCapabilities> ReadCapabilitiesAsync(Uri uri, string layer)
|
|
||||||
{
|
{
|
||||||
Stream xmlStream;
|
xmlStream = File.OpenRead(uri.OriginalString);
|
||||||
string defaultUri = null;
|
}
|
||||||
|
else if (uri.IsFile)
|
||||||
|
{
|
||||||
|
xmlStream = File.OpenRead(uri.LocalPath);
|
||||||
|
}
|
||||||
|
else if (uri.IsHttp())
|
||||||
|
{
|
||||||
|
defaultUri = uri.OriginalString.Split('?')[0];
|
||||||
|
|
||||||
if (!uri.IsAbsoluteUri)
|
xmlStream = await ImageLoader.HttpClient.GetStreamAsync(uri);
|
||||||
{
|
}
|
||||||
xmlStream = File.OpenRead(uri.OriginalString);
|
else
|
||||||
}
|
{
|
||||||
else if (uri.IsFile)
|
throw new ArgumentException($"Invalid Capabilities Uri: {uri}");
|
||||||
{
|
|
||||||
xmlStream = File.OpenRead(uri.LocalPath);
|
|
||||||
}
|
|
||||||
else if (uri.IsHttp())
|
|
||||||
{
|
|
||||||
defaultUri = uri.OriginalString.Split('?')[0];
|
|
||||||
|
|
||||||
xmlStream = await ImageLoader.HttpClient.GetStreamAsync(uri);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Invalid Capabilities Uri: {uri}");
|
|
||||||
}
|
|
||||||
|
|
||||||
using var stream = xmlStream;
|
|
||||||
|
|
||||||
var element = await XDocument.LoadRootElementAsync(stream);
|
|
||||||
|
|
||||||
return ReadCapabilities(element, layer, defaultUri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WmtsCapabilities ReadCapabilities(XElement capabilitiesElement, string layer, string defaultUri)
|
using var stream = xmlStream;
|
||||||
|
|
||||||
|
var element = await XDocument.LoadRootElementAsync(stream);
|
||||||
|
|
||||||
|
return ReadCapabilities(element, layer, defaultUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WmtsCapabilities ReadCapabilities(XElement capabilitiesElement, string layer, string defaultUri)
|
||||||
|
{
|
||||||
|
var contentsElement = capabilitiesElement.Element(wmts + "Contents") ??
|
||||||
|
throw new ArgumentException("Contents element not found.");
|
||||||
|
|
||||||
|
XElement layerElement;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(layer))
|
||||||
{
|
{
|
||||||
var contentsElement = capabilitiesElement.Element(wmts + "Contents") ??
|
layerElement = contentsElement
|
||||||
throw new ArgumentException("Contents element not found.");
|
.Elements(wmts + "Layer")
|
||||||
|
.FirstOrDefault(l => l.Element(ows + "Identifier")?.Value == layer) ??
|
||||||
|
throw new ArgumentException($"Layer element \"{layer}\" not found.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
layerElement = contentsElement
|
||||||
|
.Elements(wmts + "Layer")
|
||||||
|
.FirstOrDefault() ??
|
||||||
|
throw new ArgumentException("No Layer element found.");
|
||||||
|
|
||||||
XElement layerElement;
|
layer = layerElement.Element(ows + "Identifier")?.Value ?? "";
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(layer))
|
|
||||||
{
|
|
||||||
layerElement = contentsElement
|
|
||||||
.Elements(wmts + "Layer")
|
|
||||||
.FirstOrDefault(l => l.Element(ows + "Identifier")?.Value == layer) ??
|
|
||||||
throw new ArgumentException($"Layer element \"{layer}\" not found.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
layerElement = contentsElement
|
|
||||||
.Elements(wmts + "Layer")
|
|
||||||
.FirstOrDefault() ??
|
|
||||||
throw new ArgumentException("No Layer element found.");
|
|
||||||
|
|
||||||
layer = layerElement.Element(ows + "Identifier")?.Value ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
var styleElement = layerElement
|
|
||||||
.Elements(wmts + "Style")
|
|
||||||
.FirstOrDefault(s => s.Attribute("isDefault")?.Value == "true") ??
|
|
||||||
layerElement
|
|
||||||
.Elements(wmts + "Style")
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
var style = styleElement?.Element(ows + "Identifier")?.Value ?? "";
|
|
||||||
|
|
||||||
var uriTemplate = ReadUriTemplate(capabilitiesElement, layerElement, layer, style, defaultUri);
|
|
||||||
|
|
||||||
var tileMatrixSetIds = layerElement
|
|
||||||
.Elements(wmts + "TileMatrixSetLink")
|
|
||||||
.Select(l => l.Element(wmts + "TileMatrixSet")?.Value)
|
|
||||||
.Where(v => !string.IsNullOrEmpty(v));
|
|
||||||
|
|
||||||
var tileMatrixSets = new List<WmtsTileMatrixSet>();
|
|
||||||
|
|
||||||
foreach (var tileMatrixSetId in tileMatrixSetIds)
|
|
||||||
{
|
|
||||||
var tileMatrixSetElement = contentsElement
|
|
||||||
.Elements(wmts + "TileMatrixSet")
|
|
||||||
.FirstOrDefault(s => s.Element(ows + "Identifier")?.Value == tileMatrixSetId) ??
|
|
||||||
throw new ArgumentException($"Linked TileMatrixSet element not found in Layer \"{layer}\".");
|
|
||||||
|
|
||||||
tileMatrixSets.Add(ReadTileMatrixSet(tileMatrixSetElement, uriTemplate));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WmtsCapabilities
|
|
||||||
{
|
|
||||||
Layer = layer,
|
|
||||||
TileMatrixSets = tileMatrixSets
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ReadUriTemplate(XElement capabilitiesElement, XElement layerElement, string layer, string style, string defaultUri)
|
var styleElement = layerElement
|
||||||
|
.Elements(wmts + "Style")
|
||||||
|
.FirstOrDefault(s => s.Attribute("isDefault")?.Value == "true") ??
|
||||||
|
layerElement
|
||||||
|
.Elements(wmts + "Style")
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
var style = styleElement?.Element(ows + "Identifier")?.Value ?? "";
|
||||||
|
|
||||||
|
var uriTemplate = ReadUriTemplate(capabilitiesElement, layerElement, layer, style, defaultUri);
|
||||||
|
|
||||||
|
var tileMatrixSetIds = layerElement
|
||||||
|
.Elements(wmts + "TileMatrixSetLink")
|
||||||
|
.Select(l => l.Element(wmts + "TileMatrixSet")?.Value)
|
||||||
|
.Where(v => !string.IsNullOrEmpty(v));
|
||||||
|
|
||||||
|
var tileMatrixSets = new List<WmtsTileMatrixSet>();
|
||||||
|
|
||||||
|
foreach (var tileMatrixSetId in tileMatrixSetIds)
|
||||||
{
|
{
|
||||||
const string formatPng = "image/png";
|
var tileMatrixSetElement = contentsElement
|
||||||
const string formatJpg = "image/jpeg";
|
.Elements(wmts + "TileMatrixSet")
|
||||||
string uriTemplate = null;
|
.FirstOrDefault(s => s.Element(ows + "Identifier")?.Value == tileMatrixSetId) ??
|
||||||
|
throw new ArgumentException($"Linked TileMatrixSet element not found in Layer \"{layer}\".");
|
||||||
|
|
||||||
var resourceUrls = layerElement
|
tileMatrixSets.Add(ReadTileMatrixSet(tileMatrixSetElement, uriTemplate));
|
||||||
.Elements(wmts + "ResourceURL")
|
}
|
||||||
.Where(r => r.Attribute("resourceType")?.Value == "tile" &&
|
|
||||||
r.Attribute("format")?.Value != null &&
|
|
||||||
r.Attribute("template")?.Value != null)
|
|
||||||
.ToLookup(r => r.Attribute("format").Value,
|
|
||||||
r => r.Attribute("template").Value);
|
|
||||||
|
|
||||||
if (resourceUrls.Count != 0)
|
return new WmtsCapabilities
|
||||||
|
{
|
||||||
|
Layer = layer,
|
||||||
|
TileMatrixSets = tileMatrixSets
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ReadUriTemplate(XElement capabilitiesElement, XElement layerElement, string layer, string style, string defaultUri)
|
||||||
|
{
|
||||||
|
const string formatPng = "image/png";
|
||||||
|
const string formatJpg = "image/jpeg";
|
||||||
|
string uriTemplate = null;
|
||||||
|
|
||||||
|
var resourceUrls = layerElement
|
||||||
|
.Elements(wmts + "ResourceURL")
|
||||||
|
.Where(r => r.Attribute("resourceType")?.Value == "tile" &&
|
||||||
|
r.Attribute("format")?.Value != null &&
|
||||||
|
r.Attribute("template")?.Value != null)
|
||||||
|
.ToLookup(r => r.Attribute("format").Value,
|
||||||
|
r => r.Attribute("template").Value);
|
||||||
|
|
||||||
|
if (resourceUrls.Count != 0)
|
||||||
|
{
|
||||||
|
var uriTemplates = resourceUrls.Contains(formatPng) ? resourceUrls[formatPng]
|
||||||
|
: resourceUrls.Contains(formatJpg) ? resourceUrls[formatJpg]
|
||||||
|
: resourceUrls.First();
|
||||||
|
|
||||||
|
uriTemplate = uriTemplates.First().Replace("{Style}", style);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uriTemplate = capabilitiesElement
|
||||||
|
.Elements(ows + "OperationsMetadata")
|
||||||
|
.Elements(ows + "Operation")
|
||||||
|
.Where(o => o.Attribute("name")?.Value == "GetTile")
|
||||||
|
.Elements(ows + "DCP")
|
||||||
|
.Elements(ows + "HTTP")
|
||||||
|
.Elements(ows + "Get")
|
||||||
|
.Where(g => g.Elements(ows + "Constraint")
|
||||||
|
.Any(con => con.Attribute("name")?.Value == "GetEncoding" &&
|
||||||
|
con.Element(ows + "AllowedValues")?.Element(ows + "Value")?.Value == "KVP"))
|
||||||
|
.Select(g => g.Attribute(xlink + "href")?.Value)
|
||||||
|
.Where(h => !string.IsNullOrEmpty(h))
|
||||||
|
.Select(h => h.Split('?')[0])
|
||||||
|
.FirstOrDefault() ??
|
||||||
|
defaultUri;
|
||||||
|
|
||||||
|
if (uriTemplate != null)
|
||||||
{
|
{
|
||||||
var uriTemplates = resourceUrls.Contains(formatPng) ? resourceUrls[formatPng]
|
var formats = layerElement
|
||||||
: resourceUrls.Contains(formatJpg) ? resourceUrls[formatJpg]
|
.Elements(wmts + "Format")
|
||||||
: resourceUrls.First();
|
.Select(f => f.Value);
|
||||||
|
|
||||||
uriTemplate = uriTemplates.First().Replace("{Style}", style);
|
var format = formats.Contains(formatPng) ? formatPng
|
||||||
}
|
: formats.Contains(formatJpg) ? formatJpg
|
||||||
else
|
: formats.FirstOrDefault();
|
||||||
{
|
|
||||||
uriTemplate = capabilitiesElement
|
|
||||||
.Elements(ows + "OperationsMetadata")
|
|
||||||
.Elements(ows + "Operation")
|
|
||||||
.Where(o => o.Attribute("name")?.Value == "GetTile")
|
|
||||||
.Elements(ows + "DCP")
|
|
||||||
.Elements(ows + "HTTP")
|
|
||||||
.Elements(ows + "Get")
|
|
||||||
.Where(g => g.Elements(ows + "Constraint")
|
|
||||||
.Any(con => con.Attribute("name")?.Value == "GetEncoding" &&
|
|
||||||
con.Element(ows + "AllowedValues")?.Element(ows + "Value")?.Value == "KVP"))
|
|
||||||
.Select(g => g.Attribute(xlink + "href")?.Value)
|
|
||||||
.Where(h => !string.IsNullOrEmpty(h))
|
|
||||||
.Select(h => h.Split('?')[0])
|
|
||||||
.FirstOrDefault() ??
|
|
||||||
defaultUri;
|
|
||||||
|
|
||||||
if (uriTemplate != null)
|
if (string.IsNullOrEmpty(format))
|
||||||
{
|
{
|
||||||
var formats = layerElement
|
format = formatPng;
|
||||||
.Elements(wmts + "Format")
|
|
||||||
.Select(f => f.Value);
|
|
||||||
|
|
||||||
var format = formats.Contains(formatPng) ? formatPng
|
|
||||||
: formats.Contains(formatJpg) ? formatJpg
|
|
||||||
: formats.FirstOrDefault();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(format))
|
|
||||||
{
|
|
||||||
format = formatPng;
|
|
||||||
}
|
|
||||||
|
|
||||||
uriTemplate +=
|
|
||||||
"?Service=WMTS" +
|
|
||||||
"&Request=GetTile" +
|
|
||||||
"&Version=1.0.0" +
|
|
||||||
"&Layer=" + layer +
|
|
||||||
"&Style=" + style +
|
|
||||||
"&Format=" + format +
|
|
||||||
"&TileMatrixSet={TileMatrixSet}" +
|
|
||||||
"&TileMatrix={TileMatrix}" +
|
|
||||||
"&TileRow={TileRow}" +
|
|
||||||
"&TileCol={TileCol}";
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(uriTemplate))
|
uriTemplate +=
|
||||||
{
|
"?Service=WMTS" +
|
||||||
throw new ArgumentException($"No ResourceURL element in Layer \"{layer}\" and no GetTile KVP Operation Metadata found.");
|
"&Request=GetTile" +
|
||||||
|
"&Version=1.0.0" +
|
||||||
|
"&Layer=" + layer +
|
||||||
|
"&Style=" + style +
|
||||||
|
"&Format=" + format +
|
||||||
|
"&TileMatrixSet={TileMatrixSet}" +
|
||||||
|
"&TileMatrix={TileMatrix}" +
|
||||||
|
"&TileRow={TileRow}" +
|
||||||
|
"&TileCol={TileCol}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return uriTemplate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WmtsTileMatrixSet ReadTileMatrixSet(XElement tileMatrixSetElement, string uriTemplate)
|
if (string.IsNullOrEmpty(uriTemplate))
|
||||||
{
|
{
|
||||||
var identifier = tileMatrixSetElement.Element(ows + "Identifier")?.Value;
|
throw new ArgumentException($"No ResourceURL element in Layer \"{layer}\" and no GetTile KVP Operation Metadata found.");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(identifier))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("No Identifier element found in TileMatrixSet.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportedCrs = tileMatrixSetElement.Element(ows + "SupportedCRS")?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(supportedCrs))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"No SupportedCRS element found in TileMatrixSet \"{identifier}\".");
|
|
||||||
}
|
|
||||||
|
|
||||||
const string urnPrefix = "urn:ogc:def:crs:";
|
|
||||||
|
|
||||||
if (supportedCrs.StartsWith(urnPrefix)) // e.g. "urn:ogc:def:crs:EPSG:6.18:3857")
|
|
||||||
{
|
|
||||||
var crsComponents = supportedCrs.Substring(urnPrefix.Length).Split(':');
|
|
||||||
|
|
||||||
supportedCrs = crsComponents.First() + ":" + crsComponents.Last();
|
|
||||||
}
|
|
||||||
|
|
||||||
var tileMatrixes = new List<WmtsTileMatrix>();
|
|
||||||
|
|
||||||
foreach (var tileMatrixElement in tileMatrixSetElement.Elements(wmts + "TileMatrix"))
|
|
||||||
{
|
|
||||||
tileMatrixes.Add(ReadTileMatrix(tileMatrixElement, supportedCrs));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tileMatrixes.Count <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"No TileMatrix elements found in TileMatrixSet \"{identifier}\".");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WmtsTileMatrixSet(identifier, supportedCrs, uriTemplate, tileMatrixes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WmtsTileMatrix ReadTileMatrix(XElement tileMatrixElement, string supportedCrs)
|
return uriTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WmtsTileMatrixSet ReadTileMatrixSet(XElement tileMatrixSetElement, string uriTemplate)
|
||||||
|
{
|
||||||
|
var identifier = tileMatrixSetElement.Element(ows + "Identifier")?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(identifier))
|
||||||
{
|
{
|
||||||
var identifier = tileMatrixElement.Element(ows + "Identifier")?.Value;
|
throw new ArgumentException("No Identifier element found in TileMatrixSet.");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(identifier))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("No Identifier element found in TileMatrix.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var valueString = tileMatrixElement.Element(wmts + "ScaleDenominator")?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(valueString) ||
|
|
||||||
!double.TryParse(valueString, NumberStyles.Float, CultureInfo.InvariantCulture, out double scaleDenominator))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"No ScaleDenominator element found in TileMatrix \"{identifier}\".");
|
|
||||||
}
|
|
||||||
|
|
||||||
valueString = tileMatrixElement.Element(wmts + "TopLeftCorner")?.Value;
|
|
||||||
string[] topLeftCornerStrings;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(valueString) ||
|
|
||||||
(topLeftCornerStrings = valueString.Split([' '], StringSplitOptions.RemoveEmptyEntries)).Length < 2 ||
|
|
||||||
!double.TryParse(topLeftCornerStrings[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double left) ||
|
|
||||||
!double.TryParse(topLeftCornerStrings[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double top))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"No TopLeftCorner element found in TileMatrix \"{identifier}\".");
|
|
||||||
}
|
|
||||||
|
|
||||||
valueString = tileMatrixElement.Element(wmts + "TileWidth")?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(valueString) || !int.TryParse(valueString, out int tileWidth))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"No TileWidth element found in TileMatrix \"{identifier}\".");
|
|
||||||
}
|
|
||||||
|
|
||||||
valueString = tileMatrixElement.Element(wmts + "TileHeight")?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(valueString) || !int.TryParse(valueString, out int tileHeight))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"No TileHeight element found in TileMatrix \"{identifier}\".");
|
|
||||||
}
|
|
||||||
|
|
||||||
valueString = tileMatrixElement.Element(wmts + "MatrixWidth")?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(valueString) || !int.TryParse(valueString, out int matrixWidth))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"No MatrixWidth element found in TileMatrix \"{identifier}\".");
|
|
||||||
}
|
|
||||||
|
|
||||||
valueString = tileMatrixElement.Element(wmts + "MatrixHeight")?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(valueString) || !int.TryParse(valueString, out int matrixHeight))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"No MatrixHeight element found in TileMatrix \"{identifier}\".");
|
|
||||||
}
|
|
||||||
|
|
||||||
var topLeft = supportedCrs == "EPSG:4326"
|
|
||||||
? new Point(MapProjection.Wgs84MeterPerDegree * top, MapProjection.Wgs84MeterPerDegree * left)
|
|
||||||
: new Point(left, top);
|
|
||||||
|
|
||||||
// See 07-057r7_Web_Map_Tile_Service_Standard.pdf, section 6.1.a, page 8:
|
|
||||||
// "standardized rendering pixel size" is 0.28 mm.
|
|
||||||
//
|
|
||||||
return new WmtsTileMatrix(identifier,
|
|
||||||
1d / (scaleDenominator * 0.00028),
|
|
||||||
topLeft, tileWidth, tileHeight, matrixWidth, matrixHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var supportedCrs = tileMatrixSetElement.Element(ows + "SupportedCRS")?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(supportedCrs))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"No SupportedCRS element found in TileMatrixSet \"{identifier}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
const string urnPrefix = "urn:ogc:def:crs:";
|
||||||
|
|
||||||
|
if (supportedCrs.StartsWith(urnPrefix)) // e.g. "urn:ogc:def:crs:EPSG:6.18:3857")
|
||||||
|
{
|
||||||
|
var crsComponents = supportedCrs.Substring(urnPrefix.Length).Split(':');
|
||||||
|
|
||||||
|
supportedCrs = crsComponents.First() + ":" + crsComponents.Last();
|
||||||
|
}
|
||||||
|
|
||||||
|
var tileMatrixes = new List<WmtsTileMatrix>();
|
||||||
|
|
||||||
|
foreach (var tileMatrixElement in tileMatrixSetElement.Elements(wmts + "TileMatrix"))
|
||||||
|
{
|
||||||
|
tileMatrixes.Add(ReadTileMatrix(tileMatrixElement, supportedCrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tileMatrixes.Count <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"No TileMatrix elements found in TileMatrixSet \"{identifier}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WmtsTileMatrixSet(identifier, supportedCrs, uriTemplate, tileMatrixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WmtsTileMatrix ReadTileMatrix(XElement tileMatrixElement, string supportedCrs)
|
||||||
|
{
|
||||||
|
var identifier = tileMatrixElement.Element(ows + "Identifier")?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(identifier))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("No Identifier element found in TileMatrix.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var valueString = tileMatrixElement.Element(wmts + "ScaleDenominator")?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(valueString) ||
|
||||||
|
!double.TryParse(valueString, NumberStyles.Float, CultureInfo.InvariantCulture, out double scaleDenominator))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"No ScaleDenominator element found in TileMatrix \"{identifier}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
valueString = tileMatrixElement.Element(wmts + "TopLeftCorner")?.Value;
|
||||||
|
string[] topLeftCornerStrings;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(valueString) ||
|
||||||
|
(topLeftCornerStrings = valueString.Split([' '], StringSplitOptions.RemoveEmptyEntries)).Length < 2 ||
|
||||||
|
!double.TryParse(topLeftCornerStrings[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double left) ||
|
||||||
|
!double.TryParse(topLeftCornerStrings[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double top))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"No TopLeftCorner element found in TileMatrix \"{identifier}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
valueString = tileMatrixElement.Element(wmts + "TileWidth")?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(valueString) || !int.TryParse(valueString, out int tileWidth))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"No TileWidth element found in TileMatrix \"{identifier}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
valueString = tileMatrixElement.Element(wmts + "TileHeight")?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(valueString) || !int.TryParse(valueString, out int tileHeight))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"No TileHeight element found in TileMatrix \"{identifier}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
valueString = tileMatrixElement.Element(wmts + "MatrixWidth")?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(valueString) || !int.TryParse(valueString, out int matrixWidth))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"No MatrixWidth element found in TileMatrix \"{identifier}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
valueString = tileMatrixElement.Element(wmts + "MatrixHeight")?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(valueString) || !int.TryParse(valueString, out int matrixHeight))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"No MatrixHeight element found in TileMatrix \"{identifier}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
var topLeft = supportedCrs == "EPSG:4326"
|
||||||
|
? new Point(MapProjection.Wgs84MeterPerDegree * top, MapProjection.Wgs84MeterPerDegree * left)
|
||||||
|
: new Point(left, top);
|
||||||
|
|
||||||
|
// See 07-057r7_Web_Map_Tile_Service_Standard.pdf, section 6.1.a, page 8:
|
||||||
|
// "standardized rendering pixel size" is 0.28 mm.
|
||||||
|
//
|
||||||
|
return new WmtsTileMatrix(identifier,
|
||||||
|
1d / (scaleDenominator * 0.00028),
|
||||||
|
topLeft, tileWidth, tileHeight, matrixWidth, matrixHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,212 +15,211 @@ using Avalonia;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
{
|
|
||||||
#if WPF
|
#if WPF
|
||||||
using TileMatrixLayer = DrawingTileMatrixLayer;
|
using TileMatrixLayer = DrawingTileMatrixLayer;
|
||||||
#else
|
#else
|
||||||
using TileMatrixLayer = WmtsTileMatrixLayer;
|
using TileMatrixLayer = WmtsTileMatrixLayer;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Displays map tiles from a Web Map Tile Service (WMTS).
|
/// Displays map tiles from a Web Map Tile Service (WMTS).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class WmtsTileLayer : TilePyramidLayer
|
public partial class WmtsTileLayer : TilePyramidLayer
|
||||||
|
{
|
||||||
|
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(WmtsTileLayer));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty CapabilitiesUriProperty =
|
||||||
|
DependencyPropertyHelper.Register<WmtsTileLayer, Uri>(nameof(CapabilitiesUri));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty LayerProperty =
|
||||||
|
DependencyPropertyHelper.Register<WmtsTileLayer, string>(nameof(Layer));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty PreferredTileMatrixSetsProperty =
|
||||||
|
DependencyPropertyHelper.Register<WmtsTileLayer, string[]>(nameof(PreferredTileMatrixSets));
|
||||||
|
|
||||||
|
public WmtsTileLayer()
|
||||||
{
|
{
|
||||||
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(WmtsTileLayer));
|
Loaded += OnLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty CapabilitiesUriProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<WmtsTileLayer, Uri>(nameof(CapabilitiesUri));
|
/// The Uri of a XML file or web response that contains the service capabilities.
|
||||||
|
/// </summary>
|
||||||
|
public Uri CapabilitiesUri
|
||||||
|
{
|
||||||
|
get => (Uri)GetValue(CapabilitiesUriProperty);
|
||||||
|
set => SetValue(CapabilitiesUriProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty LayerProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<WmtsTileLayer, string>(nameof(Layer));
|
/// The Identifier of the Layer that should be displayed.
|
||||||
|
/// If not set, the first Layer defined in WMTS Capabilities is displayed.
|
||||||
|
/// </summary>
|
||||||
|
public string Layer
|
||||||
|
{
|
||||||
|
get => (string)GetValue(LayerProperty);
|
||||||
|
set => SetValue(LayerProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty PreferredTileMatrixSetsProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<WmtsTileLayer, string[]>(nameof(PreferredTileMatrixSets));
|
/// In case there are TileMatrixSets with identical SupportedCRS values,
|
||||||
|
/// the ones with Identifiers contained in this collection take precedence.
|
||||||
|
/// </summary>
|
||||||
|
public string[] PreferredTileMatrixSets
|
||||||
|
{
|
||||||
|
get => (string[])GetValue(PreferredTileMatrixSetsProperty);
|
||||||
|
set => SetValue(PreferredTileMatrixSetsProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public WmtsTileLayer()
|
/// <summary>
|
||||||
|
/// Gets a dictionary of all tile matrix sets supported by a WMTS,
|
||||||
|
/// with their CRS identifiers as dictionary keys.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, WmtsTileMatrixSet> TileMatrixSets { get; } = [];
|
||||||
|
|
||||||
|
protected IEnumerable<TileMatrixLayer> ChildLayers => Children.Cast<TileMatrixLayer>();
|
||||||
|
|
||||||
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
|
{
|
||||||
|
foreach (var layer in ChildLayers)
|
||||||
{
|
{
|
||||||
Loaded += OnLoaded;
|
layer.Measure(availableSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return new Size();
|
||||||
/// The Uri of a XML file or web response that contains the service capabilities.
|
}
|
||||||
/// </summary>
|
|
||||||
public Uri CapabilitiesUri
|
protected override Size ArrangeOverride(Size finalSize)
|
||||||
|
{
|
||||||
|
foreach (var layer in ChildLayers)
|
||||||
{
|
{
|
||||||
get => (Uri)GetValue(CapabilitiesUriProperty);
|
layer.Arrange(new Rect(0d, 0d, finalSize.Width, finalSize.Height));
|
||||||
set => SetValue(CapabilitiesUriProperty, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return finalSize;
|
||||||
/// The Identifier of the Layer that should be displayed.
|
}
|
||||||
/// If not set, the first Layer defined in WMTS Capabilities is displayed.
|
|
||||||
/// </summary>
|
protected override void UpdateRenderTransform()
|
||||||
public string Layer
|
{
|
||||||
|
foreach (var layer in ChildLayers)
|
||||||
{
|
{
|
||||||
get => (string)GetValue(LayerProperty);
|
layer.UpdateRenderTransform(ParentMap.ViewTransform);
|
||||||
set => SetValue(LayerProperty, value);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override void UpdateTileCollection()
|
||||||
/// In case there are TileMatrixSets with identical SupportedCRS values,
|
{
|
||||||
/// the ones with Identifiers contained in this collection take precedence.
|
if (ParentMap == null ||
|
||||||
/// </summary>
|
!TileMatrixSets.TryGetValue(ParentMap.MapProjection.CrsId, out WmtsTileMatrixSet tileMatrixSet))
|
||||||
public string[] PreferredTileMatrixSets
|
|
||||||
{
|
{
|
||||||
get => (string[])GetValue(PreferredTileMatrixSetsProperty);
|
|
||||||
set => SetValue(PreferredTileMatrixSetsProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a dictionary of all tile matrix sets supported by a WMTS,
|
|
||||||
/// with their CRS identifiers as dictionary keys.
|
|
||||||
/// </summary>
|
|
||||||
public Dictionary<string, WmtsTileMatrixSet> TileMatrixSets { get; } = [];
|
|
||||||
|
|
||||||
protected IEnumerable<TileMatrixLayer> ChildLayers => Children.Cast<TileMatrixLayer>();
|
|
||||||
|
|
||||||
protected override Size MeasureOverride(Size availableSize)
|
|
||||||
{
|
|
||||||
foreach (var layer in ChildLayers)
|
|
||||||
{
|
|
||||||
layer.Measure(availableSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Size();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Size ArrangeOverride(Size finalSize)
|
|
||||||
{
|
|
||||||
foreach (var layer in ChildLayers)
|
|
||||||
{
|
|
||||||
layer.Arrange(new Rect(0d, 0d, finalSize.Width, finalSize.Height));
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateRenderTransform()
|
|
||||||
{
|
|
||||||
foreach (var layer in ChildLayers)
|
|
||||||
{
|
|
||||||
layer.UpdateRenderTransform(ParentMap.ViewTransform);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateTileCollection()
|
|
||||||
{
|
|
||||||
if (ParentMap == null ||
|
|
||||||
!TileMatrixSets.TryGetValue(ParentMap.MapProjection.CrsId, out WmtsTileMatrixSet tileMatrixSet))
|
|
||||||
{
|
|
||||||
Children.Clear();
|
|
||||||
CancelLoadTiles();
|
|
||||||
}
|
|
||||||
else if (UpdateChildLayers(tileMatrixSet.TileMatrixes))
|
|
||||||
{
|
|
||||||
var tileSource = new WmtsTileSource(tileMatrixSet);
|
|
||||||
var cacheName = SourceName;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(cacheName))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(Layer))
|
|
||||||
{
|
|
||||||
cacheName += "/" + Layer.Replace(':', '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(tileMatrixSet.Identifier))
|
|
||||||
{
|
|
||||||
cacheName += "/" + tileMatrixSet.Identifier.Replace(':', '_');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BeginLoadTiles(ChildLayers.SelectMany(layer => layer.Tiles), tileSource, cacheName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool UpdateChildLayers(IList<WmtsTileMatrix> tileMatrixSet)
|
|
||||||
{
|
|
||||||
// Multiply scale by 1.001 to avoid floating point precision issues
|
|
||||||
// and get all WmtsTileMatrixes with Scale <= maxScale.
|
|
||||||
//
|
|
||||||
var maxScale = 1.001 * ParentMap.ViewTransform.Scale;
|
|
||||||
var tileMatrixes = tileMatrixSet.Where(matrix => matrix.Scale <= maxScale).ToList();
|
|
||||||
|
|
||||||
if (tileMatrixes.Count == 0)
|
|
||||||
{
|
|
||||||
Children.Clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var maxLayers = Math.Max(MaxBackgroundLevels, 0) + 1;
|
|
||||||
|
|
||||||
if (!IsBaseMapLayer)
|
|
||||||
{
|
|
||||||
// Show only the last layer.
|
|
||||||
//
|
|
||||||
tileMatrixes = tileMatrixes.GetRange(tileMatrixes.Count - 1, 1);
|
|
||||||
}
|
|
||||||
else if (tileMatrixes.Count > maxLayers)
|
|
||||||
{
|
|
||||||
// Show not more than MaxBackgroundLevels + 1 layers.
|
|
||||||
//
|
|
||||||
tileMatrixes = tileMatrixes.GetRange(tileMatrixes.Count - maxLayers, maxLayers);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get reusable layers.
|
|
||||||
//
|
|
||||||
var layers = ChildLayers.Where(layer => tileMatrixes.Contains(layer.WmtsTileMatrix)).ToList();
|
|
||||||
var tilesChanged = false;
|
|
||||||
|
|
||||||
Children.Clear();
|
Children.Clear();
|
||||||
|
CancelLoadTiles();
|
||||||
|
}
|
||||||
|
else if (UpdateChildLayers(tileMatrixSet.TileMatrixes))
|
||||||
|
{
|
||||||
|
var tileSource = new WmtsTileSource(tileMatrixSet);
|
||||||
|
var cacheName = SourceName;
|
||||||
|
|
||||||
foreach (var tileMatrix in tileMatrixes)
|
if (!string.IsNullOrEmpty(cacheName))
|
||||||
{
|
{
|
||||||
// Pass index of tileMatrix in tileMatrixSet as zoom level to TileMatrixLayer ctor.
|
if (!string.IsNullOrEmpty(Layer))
|
||||||
//
|
|
||||||
var layer = layers.FirstOrDefault(layer => layer.WmtsTileMatrix == tileMatrix) ??
|
|
||||||
new TileMatrixLayer(tileMatrix, tileMatrixSet.IndexOf(tileMatrix));
|
|
||||||
|
|
||||||
if (layer.UpdateTiles(ParentMap.ViewTransform, ParentMap.ActualWidth, ParentMap.ActualHeight))
|
|
||||||
{
|
{
|
||||||
tilesChanged = true;
|
cacheName += "/" + Layer.Replace(':', '_');
|
||||||
}
|
}
|
||||||
|
|
||||||
layer.UpdateRenderTransform(ParentMap.ViewTransform);
|
if (!string.IsNullOrEmpty(tileMatrixSet.Identifier))
|
||||||
|
{
|
||||||
Children.Add(layer);
|
cacheName += "/" + tileMatrixSet.Identifier.Replace(':', '_');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tilesChanged;
|
BeginLoadTiles(ChildLayers.SelectMany(layer => layer.Tiles), tileSource, cacheName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool UpdateChildLayers(IList<WmtsTileMatrix> tileMatrixSet)
|
||||||
|
{
|
||||||
|
// Multiply scale by 1.001 to avoid floating point precision issues
|
||||||
|
// and get all WmtsTileMatrixes with Scale <= maxScale.
|
||||||
|
//
|
||||||
|
var maxScale = 1.001 * ParentMap.ViewTransform.Scale;
|
||||||
|
var tileMatrixes = tileMatrixSet.Where(matrix => matrix.Scale <= maxScale).ToList();
|
||||||
|
|
||||||
|
if (tileMatrixes.Count == 0)
|
||||||
|
{
|
||||||
|
Children.Clear();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnLoaded(object sender, RoutedEventArgs e)
|
var maxLayers = Math.Max(MaxBackgroundLevels, 0) + 1;
|
||||||
|
|
||||||
|
if (!IsBaseMapLayer)
|
||||||
{
|
{
|
||||||
Loaded -= OnLoaded;
|
// Show only the last layer.
|
||||||
|
//
|
||||||
|
tileMatrixes = tileMatrixes.GetRange(tileMatrixes.Count - 1, 1);
|
||||||
|
}
|
||||||
|
else if (tileMatrixes.Count > maxLayers)
|
||||||
|
{
|
||||||
|
// Show not more than MaxBackgroundLevels + 1 layers.
|
||||||
|
//
|
||||||
|
tileMatrixes = tileMatrixes.GetRange(tileMatrixes.Count - maxLayers, maxLayers);
|
||||||
|
}
|
||||||
|
|
||||||
if (TileMatrixSets.Count == 0 && CapabilitiesUri != null)
|
// Get reusable layers.
|
||||||
|
//
|
||||||
|
var layers = ChildLayers.Where(layer => tileMatrixes.Contains(layer.WmtsTileMatrix)).ToList();
|
||||||
|
var tilesChanged = false;
|
||||||
|
|
||||||
|
Children.Clear();
|
||||||
|
|
||||||
|
foreach (var tileMatrix in tileMatrixes)
|
||||||
|
{
|
||||||
|
// Pass index of tileMatrix in tileMatrixSet as zoom level to TileMatrixLayer ctor.
|
||||||
|
//
|
||||||
|
var layer = layers.FirstOrDefault(layer => layer.WmtsTileMatrix == tileMatrix) ??
|
||||||
|
new TileMatrixLayer(tileMatrix, tileMatrixSet.IndexOf(tileMatrix));
|
||||||
|
|
||||||
|
if (layer.UpdateTiles(ParentMap.ViewTransform, ParentMap.ActualWidth, ParentMap.ActualHeight))
|
||||||
{
|
{
|
||||||
try
|
tilesChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.UpdateRenderTransform(ParentMap.ViewTransform);
|
||||||
|
|
||||||
|
Children.Add(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tilesChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Loaded -= OnLoaded;
|
||||||
|
|
||||||
|
if (TileMatrixSets.Count == 0 && CapabilitiesUri != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var capabilities = await WmtsCapabilities.ReadCapabilitiesAsync(CapabilitiesUri, Layer);
|
||||||
|
|
||||||
|
Layer = capabilities.Layer;
|
||||||
|
|
||||||
|
foreach (var tms in capabilities.TileMatrixSets
|
||||||
|
.Where(tms => !TileMatrixSets.ContainsKey(tms.SupportedCrsId) ||
|
||||||
|
PreferredTileMatrixSets != null &&
|
||||||
|
PreferredTileMatrixSets.Contains(tms.Identifier)))
|
||||||
{
|
{
|
||||||
var capabilities = await WmtsCapabilities.ReadCapabilitiesAsync(CapabilitiesUri, Layer);
|
TileMatrixSets[tms.SupportedCrsId] = tms;
|
||||||
|
|
||||||
Layer = capabilities.Layer;
|
|
||||||
|
|
||||||
foreach (var tms in capabilities.TileMatrixSets
|
|
||||||
.Where(tms => !TileMatrixSets.ContainsKey(tms.SupportedCrsId) ||
|
|
||||||
PreferredTileMatrixSets != null &&
|
|
||||||
PreferredTileMatrixSets.Contains(tms.Identifier)))
|
|
||||||
{
|
|
||||||
TileMatrixSets[tms.SupportedCrsId] = tms;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateTileCollection();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger?.LogError(ex, "Failed reading capabilities from {uri}", CapabilitiesUri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateTileCollection();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger?.LogError(ex, "Failed reading capabilities from {uri}", CapabilitiesUri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,21 @@ using System.Windows;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
{
|
|
||||||
public class WmtsTileMatrix(
|
|
||||||
string identifier, double scale, Point topLeft, int tileWidth, int tileHeight, int matrixWidth, int matrixHeight)
|
|
||||||
{
|
|
||||||
public string Identifier => identifier;
|
|
||||||
public double Scale => scale;
|
|
||||||
public Point TopLeft => topLeft;
|
|
||||||
public int TileWidth => tileWidth;
|
|
||||||
public int TileHeight => tileHeight;
|
|
||||||
public int MatrixWidth => matrixWidth;
|
|
||||||
public int MatrixHeight => matrixHeight;
|
|
||||||
|
|
||||||
// Indicates if the total width in meters matches the earth circumference.
|
public class WmtsTileMatrix(
|
||||||
//
|
string identifier, double scale, Point topLeft, int tileWidth, int tileHeight, int matrixWidth, int matrixHeight)
|
||||||
public bool HasFullHorizontalCoverage { get; } =
|
{
|
||||||
Math.Abs(matrixWidth * tileWidth / scale - 360d * MapProjection.Wgs84MeterPerDegree) < 1e-3;
|
public string Identifier => identifier;
|
||||||
}
|
public double Scale => scale;
|
||||||
|
public Point TopLeft => topLeft;
|
||||||
|
public int TileWidth => tileWidth;
|
||||||
|
public int TileHeight => tileHeight;
|
||||||
|
public int MatrixWidth => matrixWidth;
|
||||||
|
public int MatrixHeight => matrixHeight;
|
||||||
|
|
||||||
|
// Indicates if the total width in meters matches the earth circumference.
|
||||||
|
//
|
||||||
|
public bool HasFullHorizontalCoverage { get; } =
|
||||||
|
Math.Abs(matrixWidth * tileWidth / scale - 360d * MapProjection.Wgs84MeterPerDegree) < 1e-3;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,107 +18,106 @@ using Avalonia.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class WmtsTileMatrixLayer : Panel
|
||||||
{
|
{
|
||||||
public partial class WmtsTileMatrixLayer : Panel
|
public WmtsTileMatrixLayer(WmtsTileMatrix wmtsTileMatrix, int zoomLevel)
|
||||||
{
|
{
|
||||||
public WmtsTileMatrixLayer(WmtsTileMatrix wmtsTileMatrix, int zoomLevel)
|
this.SetRenderTransform(new MatrixTransform());
|
||||||
|
WmtsTileMatrix = wmtsTileMatrix;
|
||||||
|
TileMatrix = new TileMatrix(zoomLevel, 1, 1, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WmtsTileMatrix WmtsTileMatrix { get; }
|
||||||
|
|
||||||
|
public TileMatrix TileMatrix { get; private set; }
|
||||||
|
|
||||||
|
public IEnumerable<ImageTile> Tiles { get; private set; } = [];
|
||||||
|
|
||||||
|
public void UpdateRenderTransform(ViewTransform viewTransform)
|
||||||
|
{
|
||||||
|
// Tile matrix origin in pixels.
|
||||||
|
//
|
||||||
|
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
|
||||||
|
|
||||||
|
((MatrixTransform)RenderTransform).Matrix =
|
||||||
|
viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UpdateTiles(ViewTransform viewTransform, double viewWidth, double viewHeight)
|
||||||
|
{
|
||||||
|
// Tile matrix bounds in pixels.
|
||||||
|
//
|
||||||
|
var bounds = viewTransform.GetTileMatrixBounds(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, viewWidth, viewHeight);
|
||||||
|
|
||||||
|
// Tile X and Y bounds.
|
||||||
|
//
|
||||||
|
var xMin = (int)Math.Floor(bounds.X / WmtsTileMatrix.TileWidth);
|
||||||
|
var yMin = (int)Math.Floor(bounds.Y / WmtsTileMatrix.TileHeight);
|
||||||
|
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / WmtsTileMatrix.TileWidth);
|
||||||
|
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / WmtsTileMatrix.TileHeight);
|
||||||
|
|
||||||
|
if (!WmtsTileMatrix.HasFullHorizontalCoverage)
|
||||||
{
|
{
|
||||||
this.SetRenderTransform(new MatrixTransform());
|
// Set X range limits.
|
||||||
WmtsTileMatrix = wmtsTileMatrix;
|
|
||||||
TileMatrix = new TileMatrix(zoomLevel, 1, 1, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public WmtsTileMatrix WmtsTileMatrix { get; }
|
|
||||||
|
|
||||||
public TileMatrix TileMatrix { get; private set; }
|
|
||||||
|
|
||||||
public IEnumerable<ImageTile> Tiles { get; private set; } = [];
|
|
||||||
|
|
||||||
public void UpdateRenderTransform(ViewTransform viewTransform)
|
|
||||||
{
|
|
||||||
// Tile matrix origin in pixels.
|
|
||||||
//
|
//
|
||||||
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
|
xMin = Math.Max(xMin, 0);
|
||||||
|
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
|
||||||
((MatrixTransform)RenderTransform).Matrix =
|
|
||||||
viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool UpdateTiles(ViewTransform viewTransform, double viewWidth, double viewHeight)
|
// Set Y range limits.
|
||||||
|
//
|
||||||
|
yMin = Math.Max(yMin, 0);
|
||||||
|
yMax = Math.Min(Math.Max(yMax, 0), WmtsTileMatrix.MatrixHeight - 1);
|
||||||
|
|
||||||
|
if (TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
|
||||||
|
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
|
||||||
{
|
{
|
||||||
// Tile matrix bounds in pixels.
|
// No change of the TileMatrix and the Tiles collection.
|
||||||
//
|
//
|
||||||
var bounds = viewTransform.GetTileMatrixBounds(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, viewWidth, viewHeight);
|
return false;
|
||||||
|
|
||||||
// Tile X and Y bounds.
|
|
||||||
//
|
|
||||||
var xMin = (int)Math.Floor(bounds.X / WmtsTileMatrix.TileWidth);
|
|
||||||
var yMin = (int)Math.Floor(bounds.Y / WmtsTileMatrix.TileHeight);
|
|
||||||
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / WmtsTileMatrix.TileWidth);
|
|
||||||
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / WmtsTileMatrix.TileHeight);
|
|
||||||
|
|
||||||
if (!WmtsTileMatrix.HasFullHorizontalCoverage)
|
|
||||||
{
|
|
||||||
// Set X range limits.
|
|
||||||
//
|
|
||||||
xMin = Math.Max(xMin, 0);
|
|
||||||
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Y range limits.
|
|
||||||
//
|
|
||||||
yMin = Math.Max(yMin, 0);
|
|
||||||
yMax = Math.Min(Math.Max(yMax, 0), WmtsTileMatrix.MatrixHeight - 1);
|
|
||||||
|
|
||||||
if (TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
|
|
||||||
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
|
|
||||||
{
|
|
||||||
// No change of the TileMatrix and the Tiles collection.
|
|
||||||
//
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax);
|
|
||||||
Tiles = new ImageTileList(Tiles, TileMatrix, WmtsTileMatrix.MatrixWidth);
|
|
||||||
|
|
||||||
Children.Clear();
|
|
||||||
|
|
||||||
foreach (var tile in Tiles)
|
|
||||||
{
|
|
||||||
Children.Add(tile.Image);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Size MeasureOverride(Size availableSize)
|
TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax);
|
||||||
|
Tiles = new ImageTileList(Tiles, TileMatrix, WmtsTileMatrix.MatrixWidth);
|
||||||
|
|
||||||
|
Children.Clear();
|
||||||
|
|
||||||
|
foreach (var tile in Tiles)
|
||||||
{
|
{
|
||||||
foreach (var tile in Tiles)
|
Children.Add(tile.Image);
|
||||||
{
|
|
||||||
tile.Image.Measure(availableSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Size();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Size ArrangeOverride(Size finalSize)
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
|
{
|
||||||
|
foreach (var tile in Tiles)
|
||||||
{
|
{
|
||||||
foreach (var tile in Tiles)
|
tile.Image.Measure(availableSize);
|
||||||
{
|
|
||||||
// Arrange tiles relative to TileMatrix.XMin/YMin.
|
|
||||||
//
|
|
||||||
var width = WmtsTileMatrix.TileWidth;
|
|
||||||
var height = WmtsTileMatrix.TileHeight;
|
|
||||||
var x = width * (tile.X - TileMatrix.XMin);
|
|
||||||
var y = height * (tile.Y - TileMatrix.YMin);
|
|
||||||
|
|
||||||
tile.Image.Width = width;
|
|
||||||
tile.Image.Height = height;
|
|
||||||
tile.Image.Arrange(new Rect(x, y, width, height));
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new Size();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size ArrangeOverride(Size finalSize)
|
||||||
|
{
|
||||||
|
foreach (var tile in Tiles)
|
||||||
|
{
|
||||||
|
// Arrange tiles relative to TileMatrix.XMin/YMin.
|
||||||
|
//
|
||||||
|
var width = WmtsTileMatrix.TileWidth;
|
||||||
|
var height = WmtsTileMatrix.TileHeight;
|
||||||
|
var x = width * (tile.X - TileMatrix.XMin);
|
||||||
|
var y = height * (tile.Y - TileMatrix.YMin);
|
||||||
|
|
||||||
|
tile.Image.Width = width;
|
||||||
|
tile.Image.Height = height;
|
||||||
|
tile.Image.Arrange(new Rect(x, y, width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,42 +6,41 @@ using System.Windows;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class WmtsTileMatrixSet(
|
||||||
|
string identifier, string supportedCrsId, string uriTemplate, IEnumerable<WmtsTileMatrix> tileMatrixes)
|
||||||
{
|
{
|
||||||
public class WmtsTileMatrixSet(
|
public string Identifier => identifier;
|
||||||
string identifier, string supportedCrsId, string uriTemplate, IEnumerable<WmtsTileMatrix> tileMatrixes)
|
public string SupportedCrsId => supportedCrsId;
|
||||||
|
public string UriTemplate { get; } = uriTemplate.Replace("{TileMatrixSet}", identifier);
|
||||||
|
public List<WmtsTileMatrix> TileMatrixes { get; } = tileMatrixes.OrderBy(m => m.Scale).ToList();
|
||||||
|
|
||||||
|
public static WmtsTileMatrixSet CreateOpenStreetMapTileMatrixSet(
|
||||||
|
string uriTemplate, int minZoomLevel = 0, int maxZoomLevel = 19)
|
||||||
{
|
{
|
||||||
public string Identifier => identifier;
|
static WmtsTileMatrix CreateWmtsTileMatrix(int zoomLevel)
|
||||||
public string SupportedCrsId => supportedCrsId;
|
|
||||||
public string UriTemplate { get; } = uriTemplate.Replace("{TileMatrixSet}", identifier);
|
|
||||||
public List<WmtsTileMatrix> TileMatrixes { get; } = tileMatrixes.OrderBy(m => m.Scale).ToList();
|
|
||||||
|
|
||||||
public static WmtsTileMatrixSet CreateOpenStreetMapTileMatrixSet(
|
|
||||||
string uriTemplate, int minZoomLevel = 0, int maxZoomLevel = 19)
|
|
||||||
{
|
{
|
||||||
static WmtsTileMatrix CreateWmtsTileMatrix(int zoomLevel)
|
const int tileSize = 256;
|
||||||
{
|
const double origin = 180d * MapProjection.Wgs84MeterPerDegree;
|
||||||
const int tileSize = 256;
|
|
||||||
const double origin = 180d * MapProjection.Wgs84MeterPerDegree;
|
|
||||||
|
|
||||||
var matrixSize = 1 << zoomLevel;
|
var matrixSize = 1 << zoomLevel;
|
||||||
var scale = matrixSize * tileSize / (2d * origin);
|
var scale = matrixSize * tileSize / (2d * origin);
|
||||||
|
|
||||||
return new WmtsTileMatrix(
|
return new WmtsTileMatrix(
|
||||||
zoomLevel.ToString(), scale, new Point(-origin, origin),
|
zoomLevel.ToString(), scale, new Point(-origin, origin),
|
||||||
tileSize, tileSize, matrixSize, matrixSize);
|
tileSize, tileSize, matrixSize, matrixSize);
|
||||||
}
|
|
||||||
|
|
||||||
return new WmtsTileMatrixSet(
|
|
||||||
null,
|
|
||||||
WebMercatorProjection.DefaultCrsId,
|
|
||||||
uriTemplate
|
|
||||||
.Replace("{z}", "{0}")
|
|
||||||
.Replace("{x}", "{1}")
|
|
||||||
.Replace("{y}", "{2}"),
|
|
||||||
Enumerable
|
|
||||||
.Range(minZoomLevel, maxZoomLevel - minZoomLevel + 1)
|
|
||||||
.Select(CreateWmtsTileMatrix));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new WmtsTileMatrixSet(
|
||||||
|
null,
|
||||||
|
WebMercatorProjection.DefaultCrsId,
|
||||||
|
uriTemplate
|
||||||
|
.Replace("{z}", "{0}")
|
||||||
|
.Replace("{x}", "{1}")
|
||||||
|
.Replace("{y}", "{2}"),
|
||||||
|
Enumerable
|
||||||
|
.Range(minZoomLevel, maxZoomLevel - minZoomLevel + 1)
|
||||||
|
.Select(CreateWmtsTileMatrix));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class WmtsTileSource(WmtsTileMatrixSet tileMatrixSet) : TileSource
|
||||||
{
|
{
|
||||||
public class WmtsTileSource(WmtsTileMatrixSet tileMatrixSet) : TileSource
|
private readonly string uriFormat = tileMatrixSet.UriTemplate
|
||||||
|
.Replace("{TileMatrix}", "{0}")
|
||||||
|
.Replace("{TileCol}", "{1}")
|
||||||
|
.Replace("{TileRow}", "{2}");
|
||||||
|
|
||||||
|
private readonly List<WmtsTileMatrix> tileMatrixes = tileMatrixSet.TileMatrixes;
|
||||||
|
|
||||||
|
public override Uri GetUri(int zoomLevel, int column, int row)
|
||||||
{
|
{
|
||||||
private readonly string uriFormat = tileMatrixSet.UriTemplate
|
return zoomLevel < tileMatrixes.Count
|
||||||
.Replace("{TileMatrix}", "{0}")
|
? new Uri(string.Format(uriFormat, tileMatrixes[zoomLevel].Identifier, column, row))
|
||||||
.Replace("{TileCol}", "{1}")
|
: null;
|
||||||
.Replace("{TileRow}", "{2}");
|
|
||||||
|
|
||||||
private readonly List<WmtsTileMatrix> tileMatrixes = tileMatrixSet.TileMatrixes;
|
|
||||||
|
|
||||||
public override Uri GetUri(int zoomLevel, int column, int row)
|
|
||||||
{
|
|
||||||
return zoomLevel < tileMatrixes.Count
|
|
||||||
? new Uri(string.Format(uriFormat, tileMatrixes[zoomLevel].Identifier, column, row))
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,84 +6,83 @@ using System.Windows.Media;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Elliptical Mercator Projection - EPSG:3395.
|
||||||
|
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.44-45.
|
||||||
|
/// </summary>
|
||||||
|
public class WorldMercatorProjection : MapProjection
|
||||||
{
|
{
|
||||||
/// <summary>
|
public const string DefaultCrsId = "EPSG:3395";
|
||||||
/// Elliptical Mercator Projection - EPSG:3395.
|
|
||||||
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.44-45.
|
public WorldMercatorProjection() // parameterless constructor for XAML
|
||||||
/// </summary>
|
: this(DefaultCrsId)
|
||||||
public class WorldMercatorProjection : MapProjection
|
|
||||||
{
|
{
|
||||||
public const string DefaultCrsId = "EPSG:3395";
|
}
|
||||||
|
|
||||||
public WorldMercatorProjection() // parameterless constructor for XAML
|
public WorldMercatorProjection(string crsId)
|
||||||
: this(DefaultCrsId)
|
{
|
||||||
|
IsNormalCylindrical = true;
|
||||||
|
CrsId = crsId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
var e2 = (2d - Flattening) * Flattening;
|
||||||
|
var phi = latitude * Math.PI / 180d;
|
||||||
|
var sinPhi = Math.Sin(phi);
|
||||||
|
var k = Math.Sqrt(1d - e2 * sinPhi * sinPhi) / Math.Cos(phi); // p.44 (7-8)
|
||||||
|
|
||||||
|
return new Matrix(k, 0d, 0d, k, 0d, 0d);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Point LocationToMap(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
var x = EquatorialRadius * longitude * Math.PI / 180d;
|
||||||
|
double y;
|
||||||
|
|
||||||
|
if (latitude <= -90d)
|
||||||
{
|
{
|
||||||
|
y = double.NegativeInfinity;
|
||||||
}
|
}
|
||||||
|
else if (latitude >= 90d)
|
||||||
public WorldMercatorProjection(string crsId)
|
|
||||||
{
|
{
|
||||||
IsNormalCylindrical = true;
|
y = double.PositiveInfinity;
|
||||||
CrsId = crsId;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
|
||||||
{
|
{
|
||||||
var e2 = (2d - Flattening) * Flattening;
|
|
||||||
var phi = latitude * Math.PI / 180d;
|
var phi = latitude * Math.PI / 180d;
|
||||||
var sinPhi = Math.Sin(phi);
|
var e = Math.Sqrt((2d - Flattening) * Flattening);
|
||||||
var k = Math.Sqrt(1d - e2 * sinPhi * sinPhi) / Math.Cos(phi); // p.44 (7-8)
|
var eSinPhi = e * Math.Sin(phi);
|
||||||
|
var p = Math.Pow((1d - eSinPhi) / (1d + eSinPhi), e / 2d);
|
||||||
|
|
||||||
return new Matrix(k, 0d, 0d, k, 0d, 0d);
|
y = EquatorialRadius * Math.Log(Math.Tan(phi / 2d + Math.PI / 4d) * p); // p.44 (7-7)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Point LocationToMap(double latitude, double longitude)
|
return new Point(x, y);
|
||||||
{
|
}
|
||||||
var x = EquatorialRadius * longitude * Math.PI / 180d;
|
|
||||||
double y;
|
|
||||||
|
|
||||||
if (latitude <= -90d)
|
public override Location MapToLocation(double x, double y)
|
||||||
{
|
{
|
||||||
y = double.NegativeInfinity;
|
var t = Math.Exp(-y / EquatorialRadius); // p.44 (7-10)
|
||||||
}
|
var phi = ApproximateLatitude((2d - Flattening) * Flattening, t); // p.45 (3-5)
|
||||||
else if (latitude >= 90d)
|
var lambda = x / EquatorialRadius;
|
||||||
{
|
|
||||||
y = double.PositiveInfinity;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var phi = latitude * Math.PI / 180d;
|
|
||||||
var e = Math.Sqrt((2d - Flattening) * Flattening);
|
|
||||||
var eSinPhi = e * Math.Sin(phi);
|
|
||||||
var p = Math.Pow((1d - eSinPhi) / (1d + eSinPhi), e / 2d);
|
|
||||||
|
|
||||||
y = EquatorialRadius * Math.Log(Math.Tan(phi / 2d + Math.PI / 4d) * p); // p.44 (7-7)
|
return new Location(phi * 180d / Math.PI, lambda * 180d / Math.PI);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Point(x, y);
|
internal static double ApproximateLatitude(double e2, double t)
|
||||||
}
|
{
|
||||||
|
var e4 = e2 * e2;
|
||||||
|
var e6 = e2 * e4;
|
||||||
|
var e8 = e2 * e6;
|
||||||
|
var chi = Math.PI / 2d - 2d * Math.Atan(t); // p.45 (7-13)
|
||||||
|
|
||||||
public override Location MapToLocation(double x, double y)
|
return chi +
|
||||||
{
|
(e2 / 2d + e4 * 5d / 24d + e6 / 12d + e8 * 13d / 360d) * Math.Sin(2d * chi) +
|
||||||
var t = Math.Exp(-y / EquatorialRadius); // p.44 (7-10)
|
(e4 * 7d / 48d + e6 * 29d / 240d + e8 * 811d / 11520d) * Math.Sin(4d * chi) +
|
||||||
var phi = ApproximateLatitude((2d - Flattening) * Flattening, t); // p.45 (3-5)
|
(e6 * 7d / 120d + e8 * 81d / 1120d) * Math.Sin(6d * chi) +
|
||||||
var lambda = x / EquatorialRadius;
|
e8 * 4279d / 161280d * Math.Sin(8d * chi); // p.45 (3-5)
|
||||||
|
|
||||||
return new Location(phi * 180d / Math.PI, lambda * 180d / Math.PI);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static double ApproximateLatitude(double e2, double t)
|
|
||||||
{
|
|
||||||
var e4 = e2 * e2;
|
|
||||||
var e6 = e2 * e4;
|
|
||||||
var e8 = e2 * e6;
|
|
||||||
var chi = Math.PI / 2d - 2d * Math.Atan(t); // p.45 (7-13)
|
|
||||||
|
|
||||||
return chi +
|
|
||||||
(e2 / 2d + e4 * 5d / 24d + e6 / 12d + e8 * 13d / 360d) * Math.Sin(2d * chi) +
|
|
||||||
(e4 * 7d / 48d + e6 * 29d / 240d + e8 * 811d / 11520d) * Math.Sin(4d * chi) +
|
|
||||||
(e6 * 7d / 120d + e8 * 81d / 1120d) * Math.Sin(6d * chi) +
|
|
||||||
e8 * 4279d / 161280d * Math.Sin(8d * chi); // p.45 (3-5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,17 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
internal static class XDocument
|
||||||
{
|
{
|
||||||
internal static class XDocument
|
public static async Task<XElement> LoadRootElementAsync(Stream stream)
|
||||||
{
|
{
|
||||||
public static async Task<XElement> LoadRootElementAsync(Stream stream)
|
|
||||||
{
|
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
var document = await Task.Run(() => System.Xml.Linq.XDocument.Load(stream, LoadOptions.None));
|
var document = await Task.Run(() => System.Xml.Linq.XDocument.Load(stream, LoadOptions.None));
|
||||||
#else
|
#else
|
||||||
var document = await System.Xml.Linq.XDocument.LoadAsync(stream, LoadOptions.None, System.Threading.CancellationToken.None);
|
var document = await System.Xml.Linq.XDocument.LoadAsync(stream, LoadOptions.None, System.Threading.CancellationToken.None);
|
||||||
#endif
|
#endif
|
||||||
return document.Root;
|
return document.Root;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,169 +6,168 @@ using System.Windows;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class BitmapTile(int zoomLevel, int x, int y, int columnCount, int width, int height)
|
||||||
|
: Tile(zoomLevel, x, y, columnCount)
|
||||||
{
|
{
|
||||||
public class BitmapTile(int zoomLevel, int x, int y, int columnCount, int width, int height)
|
public event EventHandler Completed;
|
||||||
: Tile(zoomLevel, x, y, columnCount)
|
|
||||||
|
public byte[] PixelBuffer { get; set; }
|
||||||
|
|
||||||
|
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
|
||||||
{
|
{
|
||||||
public event EventHandler Completed;
|
var image = await loadImageFunc().ConfigureAwait(false);
|
||||||
|
|
||||||
public byte[] PixelBuffer { get; set; }
|
if (image is BitmapSource bitmap)
|
||||||
|
|
||||||
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
|
|
||||||
{
|
{
|
||||||
var image = await loadImageFunc().ConfigureAwait(false);
|
if (bitmap.Format != PixelFormats.Pbgra32)
|
||||||
|
|
||||||
if (image is BitmapSource bitmap)
|
|
||||||
{
|
{
|
||||||
if (bitmap.Format != PixelFormats.Pbgra32)
|
bitmap = new FormatConvertedBitmap(bitmap, PixelFormats.Pbgra32, null, 0d);
|
||||||
{
|
|
||||||
bitmap = new FormatConvertedBitmap(bitmap, PixelFormats.Pbgra32, null, 0d);
|
|
||||||
}
|
|
||||||
|
|
||||||
PixelBuffer = new byte[4 * width * height];
|
|
||||||
bitmap.CopyPixels(PixelBuffer, 4 * width, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Completed?.Invoke(this, EventArgs.Empty);
|
PixelBuffer = new byte[4 * width * height];
|
||||||
|
bitmap.CopyPixels(PixelBuffer, 4 * width, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Completed?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BitmapTileMatrixLayer(WmtsTileMatrix wmtsTileMatrix, int zoomLevel) : UIElement
|
||||||
|
{
|
||||||
|
private readonly MatrixTransform transform = new MatrixTransform();
|
||||||
|
|
||||||
|
private WriteableBitmap bitmap;
|
||||||
|
|
||||||
|
public WmtsTileMatrix WmtsTileMatrix => wmtsTileMatrix;
|
||||||
|
|
||||||
|
public TileMatrix TileMatrix { get; private set; } = new TileMatrix(zoomLevel, 1, 1, 0, 0);
|
||||||
|
|
||||||
|
public IEnumerable<BitmapTile> Tiles { get; private set; } = [];
|
||||||
|
|
||||||
|
protected override void OnRender(DrawingContext drawingContext)
|
||||||
|
{
|
||||||
|
drawingContext.PushTransform(transform);
|
||||||
|
drawingContext.DrawImage(bitmap, new Rect(0d, 0d, bitmap.PixelWidth, bitmap.PixelHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BitmapTileMatrixLayer(WmtsTileMatrix wmtsTileMatrix, int zoomLevel) : UIElement
|
public void UpdateRenderTransform(ViewTransform viewTransform)
|
||||||
{
|
{
|
||||||
private readonly MatrixTransform transform = new MatrixTransform();
|
// Tile matrix origin in pixels.
|
||||||
|
//
|
||||||
|
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
|
||||||
|
|
||||||
private WriteableBitmap bitmap;
|
transform.Matrix = viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
public WmtsTileMatrix WmtsTileMatrix => wmtsTileMatrix;
|
public bool UpdateTiles(ViewTransform viewTransform, double viewWidth, double viewHeight)
|
||||||
|
{
|
||||||
|
// Tile matrix bounds in pixels.
|
||||||
|
//
|
||||||
|
var bounds = viewTransform.GetTileMatrixBounds(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, viewWidth, viewHeight);
|
||||||
|
|
||||||
public TileMatrix TileMatrix { get; private set; } = new TileMatrix(zoomLevel, 1, 1, 0, 0);
|
// Tile X and Y bounds.
|
||||||
|
//
|
||||||
|
var xMin = (int)Math.Floor(bounds.X / WmtsTileMatrix.TileWidth);
|
||||||
|
var yMin = (int)Math.Floor(bounds.Y / WmtsTileMatrix.TileHeight);
|
||||||
|
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / WmtsTileMatrix.TileWidth);
|
||||||
|
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / WmtsTileMatrix.TileHeight);
|
||||||
|
|
||||||
public IEnumerable<BitmapTile> Tiles { get; private set; } = [];
|
if (!WmtsTileMatrix.HasFullHorizontalCoverage)
|
||||||
|
|
||||||
protected override void OnRender(DrawingContext drawingContext)
|
|
||||||
{
|
{
|
||||||
drawingContext.PushTransform(transform);
|
// Set X range limits.
|
||||||
drawingContext.DrawImage(bitmap, new Rect(0d, 0d, bitmap.PixelWidth, bitmap.PixelHeight));
|
//
|
||||||
|
xMin = Math.Max(xMin, 0);
|
||||||
|
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateRenderTransform(ViewTransform viewTransform)
|
// Set Y range limits.
|
||||||
{
|
//
|
||||||
// Tile matrix origin in pixels.
|
yMin = Math.Max(yMin, 0);
|
||||||
//
|
yMax = Math.Min(Math.Max(yMax, 0), WmtsTileMatrix.MatrixHeight - 1);
|
||||||
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
|
|
||||||
|
|
||||||
transform.Matrix = viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
|
if (TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
|
||||||
|
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
|
||||||
|
{
|
||||||
|
// No change of the TileMatrix and the Tiles collection.
|
||||||
|
//
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool UpdateTiles(ViewTransform viewTransform, double viewWidth, double viewHeight)
|
TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax);
|
||||||
|
|
||||||
|
bitmap = new WriteableBitmap(
|
||||||
|
WmtsTileMatrix.TileWidth * TileMatrix.Width,
|
||||||
|
WmtsTileMatrix.TileHeight * TileMatrix.Height,
|
||||||
|
96, 96, PixelFormats.Pbgra32, null);
|
||||||
|
|
||||||
|
CreateTiles();
|
||||||
|
InvalidateVisual();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateTiles()
|
||||||
|
{
|
||||||
|
var tiles = new List<BitmapTile>(TileMatrix.Width * TileMatrix.Height);
|
||||||
|
|
||||||
|
for (var y = TileMatrix.YMin; y <= TileMatrix.YMax; y++)
|
||||||
{
|
{
|
||||||
// Tile matrix bounds in pixels.
|
for (var x = TileMatrix.XMin; x <= TileMatrix.XMax; x++)
|
||||||
//
|
|
||||||
var bounds = viewTransform.GetTileMatrixBounds(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, viewWidth, viewHeight);
|
|
||||||
|
|
||||||
// Tile X and Y bounds.
|
|
||||||
//
|
|
||||||
var xMin = (int)Math.Floor(bounds.X / WmtsTileMatrix.TileWidth);
|
|
||||||
var yMin = (int)Math.Floor(bounds.Y / WmtsTileMatrix.TileHeight);
|
|
||||||
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / WmtsTileMatrix.TileWidth);
|
|
||||||
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / WmtsTileMatrix.TileHeight);
|
|
||||||
|
|
||||||
if (!WmtsTileMatrix.HasFullHorizontalCoverage)
|
|
||||||
{
|
{
|
||||||
// Set X range limits.
|
var tile = Tiles.FirstOrDefault(t => t.X == x && t.Y == y);
|
||||||
//
|
|
||||||
xMin = Math.Max(xMin, 0);
|
|
||||||
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Y range limits.
|
if (tile == null)
|
||||||
//
|
|
||||||
yMin = Math.Max(yMin, 0);
|
|
||||||
yMax = Math.Min(Math.Max(yMax, 0), WmtsTileMatrix.MatrixHeight - 1);
|
|
||||||
|
|
||||||
if (TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
|
|
||||||
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
|
|
||||||
{
|
|
||||||
// No change of the TileMatrix and the Tiles collection.
|
|
||||||
//
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax);
|
|
||||||
|
|
||||||
bitmap = new WriteableBitmap(
|
|
||||||
WmtsTileMatrix.TileWidth * TileMatrix.Width,
|
|
||||||
WmtsTileMatrix.TileHeight * TileMatrix.Height,
|
|
||||||
96, 96, PixelFormats.Pbgra32, null);
|
|
||||||
|
|
||||||
CreateTiles();
|
|
||||||
InvalidateVisual();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateTiles()
|
|
||||||
{
|
|
||||||
var tiles = new List<BitmapTile>(TileMatrix.Width * TileMatrix.Height);
|
|
||||||
|
|
||||||
for (var y = TileMatrix.YMin; y <= TileMatrix.YMax; y++)
|
|
||||||
{
|
|
||||||
for (var x = TileMatrix.XMin; x <= TileMatrix.XMax; x++)
|
|
||||||
{
|
{
|
||||||
var tile = Tiles.FirstOrDefault(t => t.X == x && t.Y == y);
|
tile = new BitmapTile(TileMatrix.ZoomLevel, x, y, WmtsTileMatrix.MatrixWidth, WmtsTileMatrix.TileWidth, WmtsTileMatrix.TileHeight);
|
||||||
|
|
||||||
if (tile == null)
|
var equivalentTile = Tiles.FirstOrDefault(t => t.PixelBuffer != null && t.Column == tile.Column && t.Row == tile.Row);
|
||||||
|
|
||||||
|
if (equivalentTile != null)
|
||||||
{
|
{
|
||||||
tile = new BitmapTile(TileMatrix.ZoomLevel, x, y, WmtsTileMatrix.MatrixWidth, WmtsTileMatrix.TileWidth, WmtsTileMatrix.TileHeight);
|
tile.IsPending = false;
|
||||||
|
tile.PixelBuffer = equivalentTile.PixelBuffer;
|
||||||
var equivalentTile = Tiles.FirstOrDefault(t => t.PixelBuffer != null && t.Column == tile.Column && t.Row == tile.Row);
|
|
||||||
|
|
||||||
if (equivalentTile != null)
|
|
||||||
{
|
|
||||||
tile.IsPending = false;
|
|
||||||
tile.PixelBuffer = equivalentTile.PixelBuffer;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tile.Completed += OnTileCompleted;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (tile.PixelBuffer != null)
|
|
||||||
{
|
{
|
||||||
CopyTile(tile);
|
tile.Completed += OnTileCompleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles.Add(tile);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Tiles = tiles;
|
if (tile.PixelBuffer != null)
|
||||||
|
{
|
||||||
|
CopyTile(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles.Add(tile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CopyTile(BitmapTile tile)
|
Tiles = tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyTile(BitmapTile tile)
|
||||||
|
{
|
||||||
|
var width = WmtsTileMatrix.TileWidth;
|
||||||
|
var height = WmtsTileMatrix.TileHeight;
|
||||||
|
var x = width * (tile.X - TileMatrix.XMin);
|
||||||
|
var y = height * (tile.Y - TileMatrix.YMin);
|
||||||
|
|
||||||
|
bitmap.WritePixels(new Int32Rect(x, y, width, height), tile.PixelBuffer, 4 * WmtsTileMatrix.TileWidth, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTileCompleted(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var tile = (BitmapTile)sender;
|
||||||
|
|
||||||
|
tile.Completed -= OnTileCompleted;
|
||||||
|
|
||||||
|
if (tile.X >= TileMatrix.XMin && tile.X <= TileMatrix.XMax &&
|
||||||
|
tile.Y >= TileMatrix.YMin && tile.Y <= TileMatrix.YMax &&
|
||||||
|
tile.PixelBuffer != null)
|
||||||
{
|
{
|
||||||
var width = WmtsTileMatrix.TileWidth;
|
_ = Dispatcher.InvokeAsync(() => CopyTile(tile));
|
||||||
var height = WmtsTileMatrix.TileHeight;
|
|
||||||
var x = width * (tile.X - TileMatrix.XMin);
|
|
||||||
var y = height * (tile.Y - TileMatrix.YMin);
|
|
||||||
|
|
||||||
bitmap.WritePixels(new Int32Rect(x, y, width, height), tile.PixelBuffer, 4 * WmtsTileMatrix.TileWidth, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTileCompleted(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var tile = (BitmapTile)sender;
|
|
||||||
|
|
||||||
tile.Completed -= OnTileCompleted;
|
|
||||||
|
|
||||||
if (tile.X >= TileMatrix.XMin && tile.X <= TileMatrix.XMax &&
|
|
||||||
tile.Y >= TileMatrix.YMin && tile.Y <= TileMatrix.YMax &&
|
|
||||||
tile.PixelBuffer != null)
|
|
||||||
{
|
|
||||||
_ = Dispatcher.InvokeAsync(() => CopyTile(tile));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,127 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public static class DependencyPropertyHelper
|
||||||
{
|
{
|
||||||
public static class DependencyPropertyHelper
|
public static DependencyProperty RegisterAttached<TValue>(
|
||||||
|
string name,
|
||||||
|
Type ownerType,
|
||||||
|
TValue defaultValue = default,
|
||||||
|
Action<FrameworkElement, TValue, TValue> changed = null,
|
||||||
|
bool inherits = false)
|
||||||
{
|
{
|
||||||
public static DependencyProperty RegisterAttached<TValue>(
|
var metadata = new FrameworkPropertyMetadata
|
||||||
string name,
|
|
||||||
Type ownerType,
|
|
||||||
TValue defaultValue = default,
|
|
||||||
Action<FrameworkElement, TValue, TValue> changed = null,
|
|
||||||
bool inherits = false)
|
|
||||||
{
|
{
|
||||||
var metadata = new FrameworkPropertyMetadata
|
DefaultValue = defaultValue,
|
||||||
{
|
Inherits = inherits
|
||||||
DefaultValue = defaultValue,
|
};
|
||||||
Inherits = inherits
|
|
||||||
};
|
|
||||||
|
|
||||||
if (changed != null)
|
if (changed != null)
|
||||||
|
{
|
||||||
|
metadata.PropertyChangedCallback = (o, e) =>
|
||||||
{
|
{
|
||||||
metadata.PropertyChangedCallback = (o, e) =>
|
if (o is FrameworkElement element)
|
||||||
{
|
{
|
||||||
if (o is FrameworkElement element)
|
changed(element, (TValue)e.OldValue, (TValue)e.NewValue);
|
||||||
{
|
}
|
||||||
changed(element, (TValue)e.OldValue, (TValue)e.NewValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return DependencyProperty.RegisterAttached(name, typeof(TValue), ownerType, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DependencyProperty RegisterAttached<TValue>(
|
|
||||||
string name,
|
|
||||||
Type ownerType,
|
|
||||||
TValue defaultValue,
|
|
||||||
FrameworkPropertyMetadataOptions options)
|
|
||||||
{
|
|
||||||
var metadata = new FrameworkPropertyMetadata(defaultValue, options);
|
|
||||||
|
|
||||||
return DependencyProperty.RegisterAttached(name, typeof(TValue), ownerType, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DependencyProperty Register<TOwner, TValue>(
|
|
||||||
string name,
|
|
||||||
TValue defaultValue,
|
|
||||||
FrameworkPropertyMetadataOptions options)
|
|
||||||
where TOwner : DependencyObject
|
|
||||||
{
|
|
||||||
var metadata = new FrameworkPropertyMetadata(defaultValue, options);
|
|
||||||
|
|
||||||
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DependencyProperty Register<TOwner, TValue>(
|
|
||||||
string name,
|
|
||||||
TValue defaultValue = default,
|
|
||||||
Action<TOwner, TValue, TValue> changed = null,
|
|
||||||
Func<TOwner, TValue, TValue> coerce = null,
|
|
||||||
bool bindTwoWayByDefault = false)
|
|
||||||
where TOwner : DependencyObject
|
|
||||||
{
|
|
||||||
var metadata = new FrameworkPropertyMetadata
|
|
||||||
{
|
|
||||||
DefaultValue = defaultValue,
|
|
||||||
BindsTwoWayByDefault = bindTwoWayByDefault
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (changed != null)
|
|
||||||
{
|
|
||||||
metadata.PropertyChangedCallback = (o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coerce != null)
|
|
||||||
{
|
|
||||||
metadata.CoerceValueCallback = (o, v) => coerce((TOwner)o, (TValue)v);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DependencyPropertyKey RegisterReadOnly<TOwner, TValue>(
|
return DependencyProperty.RegisterAttached(name, typeof(TValue), ownerType, metadata);
|
||||||
string name,
|
}
|
||||||
TValue defaultValue = default)
|
|
||||||
where TOwner : DependencyObject
|
public static DependencyProperty RegisterAttached<TValue>(
|
||||||
|
string name,
|
||||||
|
Type ownerType,
|
||||||
|
TValue defaultValue,
|
||||||
|
FrameworkPropertyMetadataOptions options)
|
||||||
|
{
|
||||||
|
var metadata = new FrameworkPropertyMetadata(defaultValue, options);
|
||||||
|
|
||||||
|
return DependencyProperty.RegisterAttached(name, typeof(TValue), ownerType, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DependencyProperty Register<TOwner, TValue>(
|
||||||
|
string name,
|
||||||
|
TValue defaultValue,
|
||||||
|
FrameworkPropertyMetadataOptions options)
|
||||||
|
where TOwner : DependencyObject
|
||||||
|
{
|
||||||
|
var metadata = new FrameworkPropertyMetadata(defaultValue, options);
|
||||||
|
|
||||||
|
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DependencyProperty Register<TOwner, TValue>(
|
||||||
|
string name,
|
||||||
|
TValue defaultValue = default,
|
||||||
|
Action<TOwner, TValue, TValue> changed = null,
|
||||||
|
Func<TOwner, TValue, TValue> coerce = null,
|
||||||
|
bool bindTwoWayByDefault = false)
|
||||||
|
where TOwner : DependencyObject
|
||||||
|
{
|
||||||
|
var metadata = new FrameworkPropertyMetadata
|
||||||
{
|
{
|
||||||
return DependencyProperty.RegisterReadOnly(name, typeof(TValue), typeof(TOwner), new PropertyMetadata(defaultValue));
|
DefaultValue = defaultValue,
|
||||||
|
BindsTwoWayByDefault = bindTwoWayByDefault
|
||||||
|
};
|
||||||
|
|
||||||
|
if (changed != null)
|
||||||
|
{
|
||||||
|
metadata.PropertyChangedCallback = (o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DependencyProperty AddOwner<TOwner, TValue>(
|
if (coerce != null)
|
||||||
DependencyProperty source) where TOwner : DependencyObject
|
|
||||||
{
|
{
|
||||||
return source.AddOwner(typeof(TOwner));
|
metadata.CoerceValueCallback = (o, v) => coerce((TOwner)o, (TValue)v);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DependencyProperty AddOwner<TOwner, TValue>(
|
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
|
||||||
DependencyProperty source,
|
}
|
||||||
TValue defaultValue) where TOwner : DependencyObject
|
|
||||||
{
|
|
||||||
return source.AddOwner(typeof(TOwner), new FrameworkPropertyMetadata(defaultValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DependencyProperty AddOwner<TOwner, TValue>(
|
public static DependencyPropertyKey RegisterReadOnly<TOwner, TValue>(
|
||||||
DependencyProperty source,
|
string name,
|
||||||
Action<TOwner, TValue, TValue> changed) where TOwner : DependencyObject
|
TValue defaultValue = default)
|
||||||
{
|
where TOwner : DependencyObject
|
||||||
return source.AddOwner(typeof(TOwner), new FrameworkPropertyMetadata(
|
{
|
||||||
(o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue)));
|
return DependencyProperty.RegisterReadOnly(name, typeof(TValue), typeof(TOwner), new PropertyMetadata(defaultValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DependencyProperty AddOwner<TOwner, TValue>(
|
public static DependencyProperty AddOwner<TOwner, TValue>(
|
||||||
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
|
DependencyProperty source) where TOwner : DependencyObject
|
||||||
DependencyProperty source) where TOwner : DependencyObject
|
{
|
||||||
{
|
return source.AddOwner(typeof(TOwner));
|
||||||
return AddOwner<TOwner, TValue>(source);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static DependencyProperty AddOwner<TOwner, TValue>(
|
public static DependencyProperty AddOwner<TOwner, TValue>(
|
||||||
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
|
DependencyProperty source,
|
||||||
DependencyProperty source,
|
TValue defaultValue) where TOwner : DependencyObject
|
||||||
Action<TOwner, TValue, TValue> changed) where TOwner : DependencyObject
|
{
|
||||||
{
|
return source.AddOwner(typeof(TOwner), new FrameworkPropertyMetadata(defaultValue));
|
||||||
return AddOwner(source, changed);
|
}
|
||||||
}
|
|
||||||
|
public static DependencyProperty AddOwner<TOwner, TValue>(
|
||||||
|
DependencyProperty source,
|
||||||
|
Action<TOwner, TValue, TValue> changed) where TOwner : DependencyObject
|
||||||
|
{
|
||||||
|
return source.AddOwner(typeof(TOwner), new FrameworkPropertyMetadata(
|
||||||
|
(o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DependencyProperty AddOwner<TOwner, TValue>(
|
||||||
|
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
|
||||||
|
DependencyProperty source) where TOwner : DependencyObject
|
||||||
|
{
|
||||||
|
return AddOwner<TOwner, TValue>(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DependencyProperty AddOwner<TOwner, TValue>(
|
||||||
|
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
|
||||||
|
DependencyProperty source,
|
||||||
|
Action<TOwner, TValue, TValue> changed) where TOwner : DependencyObject
|
||||||
|
{
|
||||||
|
return AddOwner(source, changed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,75 +4,74 @@ using System.Windows.Media;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class DrawingTile : Tile
|
||||||
{
|
{
|
||||||
public class DrawingTile : Tile
|
public DrawingTile(int zoomLevel, int x, int y, int columnCount)
|
||||||
|
: base(zoomLevel, x, y, columnCount)
|
||||||
{
|
{
|
||||||
public DrawingTile(int zoomLevel, int x, int y, int columnCount)
|
Drawing.Children.Add(ImageDrawing);
|
||||||
: base(zoomLevel, x, y, columnCount)
|
}
|
||||||
|
|
||||||
|
public DrawingGroup Drawing { get; } = new DrawingGroup();
|
||||||
|
|
||||||
|
public ImageDrawing ImageDrawing { get; } = new ImageDrawing();
|
||||||
|
|
||||||
|
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
|
||||||
|
{
|
||||||
|
var image = await loadImageFunc().ConfigureAwait(false);
|
||||||
|
|
||||||
|
void SetImageSource()
|
||||||
{
|
{
|
||||||
Drawing.Children.Add(ImageDrawing);
|
ImageDrawing.ImageSource = image;
|
||||||
}
|
|
||||||
|
|
||||||
public DrawingGroup Drawing { get; } = new DrawingGroup();
|
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
|
||||||
|
|
||||||
public ImageDrawing ImageDrawing { get; } = new ImageDrawing();
|
|
||||||
|
|
||||||
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
|
|
||||||
{
|
|
||||||
var image = await loadImageFunc().ConfigureAwait(false);
|
|
||||||
|
|
||||||
void SetImageSource()
|
|
||||||
{
|
{
|
||||||
ImageDrawing.ImageSource = image;
|
if (image is BitmapSource bitmap && !bitmap.IsFrozen && bitmap.IsDownloading)
|
||||||
|
|
||||||
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
|
|
||||||
{
|
{
|
||||||
if (image is BitmapSource bitmap && !bitmap.IsFrozen && bitmap.IsDownloading)
|
bitmap.DownloadCompleted += BitmapDownloadCompleted;
|
||||||
{
|
bitmap.DownloadFailed += BitmapDownloadFailed;
|
||||||
bitmap.DownloadCompleted += BitmapDownloadCompleted;
|
}
|
||||||
bitmap.DownloadFailed += BitmapDownloadFailed;
|
else
|
||||||
}
|
{
|
||||||
else
|
BeginFadeInAnimation();
|
||||||
{
|
|
||||||
BeginFadeInAnimation();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Drawing.Dispatcher.InvokeAsync(SetImageSource);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BeginFadeInAnimation()
|
await Drawing.Dispatcher.InvokeAsync(SetImageSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginFadeInAnimation()
|
||||||
|
{
|
||||||
|
var fadeInAnimation = new DoubleAnimation
|
||||||
{
|
{
|
||||||
var fadeInAnimation = new DoubleAnimation
|
From = 0d,
|
||||||
{
|
Duration = MapBase.ImageFadeDuration,
|
||||||
From = 0d,
|
FillBehavior = FillBehavior.Stop
|
||||||
Duration = MapBase.ImageFadeDuration,
|
};
|
||||||
FillBehavior = FillBehavior.Stop
|
|
||||||
};
|
|
||||||
|
|
||||||
Drawing.BeginAnimation(DrawingGroup.OpacityProperty, fadeInAnimation);
|
Drawing.BeginAnimation(DrawingGroup.OpacityProperty, fadeInAnimation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BitmapDownloadCompleted(object sender, EventArgs e)
|
private void BitmapDownloadCompleted(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var bitmap = (BitmapSource)sender;
|
var bitmap = (BitmapSource)sender;
|
||||||
|
|
||||||
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
|
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
|
||||||
bitmap.DownloadFailed -= BitmapDownloadFailed;
|
bitmap.DownloadFailed -= BitmapDownloadFailed;
|
||||||
|
|
||||||
BeginFadeInAnimation();
|
BeginFadeInAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)
|
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)
|
||||||
{
|
{
|
||||||
var bitmap = (BitmapSource)sender;
|
var bitmap = (BitmapSource)sender;
|
||||||
|
|
||||||
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
|
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
|
||||||
bitmap.DownloadFailed -= BitmapDownloadFailed;
|
bitmap.DownloadFailed -= BitmapDownloadFailed;
|
||||||
|
|
||||||
ImageDrawing.ImageSource = null;
|
ImageDrawing.ImageSource = null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,113 +4,112 @@ using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class DrawingTileMatrixLayer(WmtsTileMatrix wmtsTileMatrix, int zoomLevel) : UIElement
|
||||||
{
|
{
|
||||||
public class DrawingTileMatrixLayer(WmtsTileMatrix wmtsTileMatrix, int zoomLevel) : UIElement
|
private readonly MatrixTransform transform = new MatrixTransform();
|
||||||
|
|
||||||
|
public WmtsTileMatrix WmtsTileMatrix => wmtsTileMatrix;
|
||||||
|
|
||||||
|
public TileMatrix TileMatrix { get; private set; } = new TileMatrix(zoomLevel, 1, 1, 0, 0);
|
||||||
|
|
||||||
|
public IEnumerable<DrawingTile> Tiles { get; private set; } = [];
|
||||||
|
|
||||||
|
protected override void OnRender(DrawingContext drawingContext)
|
||||||
{
|
{
|
||||||
private readonly MatrixTransform transform = new MatrixTransform();
|
drawingContext.PushTransform(transform);
|
||||||
|
|
||||||
public WmtsTileMatrix WmtsTileMatrix => wmtsTileMatrix;
|
foreach (var tile in Tiles)
|
||||||
|
|
||||||
public TileMatrix TileMatrix { get; private set; } = new TileMatrix(zoomLevel, 1, 1, 0, 0);
|
|
||||||
|
|
||||||
public IEnumerable<DrawingTile> Tiles { get; private set; } = [];
|
|
||||||
|
|
||||||
protected override void OnRender(DrawingContext drawingContext)
|
|
||||||
{
|
{
|
||||||
drawingContext.PushTransform(transform);
|
drawingContext.DrawDrawing(tile.Drawing);
|
||||||
|
|
||||||
foreach (var tile in Tiles)
|
|
||||||
{
|
|
||||||
drawingContext.DrawDrawing(tile.Drawing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateRenderTransform(ViewTransform viewTransform)
|
|
||||||
{
|
|
||||||
// Tile matrix origin in pixels.
|
|
||||||
//
|
|
||||||
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
|
|
||||||
|
|
||||||
transform.Matrix = viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool UpdateTiles(ViewTransform viewTransform, double viewWidth, double viewHeight)
|
|
||||||
{
|
|
||||||
// Tile matrix bounds in pixels.
|
|
||||||
//
|
|
||||||
var bounds = viewTransform.GetTileMatrixBounds(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, viewWidth, viewHeight);
|
|
||||||
|
|
||||||
// Tile X and Y bounds.
|
|
||||||
//
|
|
||||||
var xMin = (int)Math.Floor(bounds.X / WmtsTileMatrix.TileWidth);
|
|
||||||
var yMin = (int)Math.Floor(bounds.Y / WmtsTileMatrix.TileHeight);
|
|
||||||
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / WmtsTileMatrix.TileWidth);
|
|
||||||
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / WmtsTileMatrix.TileHeight);
|
|
||||||
|
|
||||||
if (!WmtsTileMatrix.HasFullHorizontalCoverage)
|
|
||||||
{
|
|
||||||
// Set X range limits.
|
|
||||||
//
|
|
||||||
xMin = Math.Max(xMin, 0);
|
|
||||||
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Y range limits.
|
|
||||||
//
|
|
||||||
yMin = Math.Max(yMin, 0);
|
|
||||||
yMax = Math.Min(Math.Max(yMax, 0), WmtsTileMatrix.MatrixHeight - 1);
|
|
||||||
|
|
||||||
if (TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
|
|
||||||
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
|
|
||||||
{
|
|
||||||
// No change of the TileMatrix and the Tiles collection.
|
|
||||||
//
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax);
|
|
||||||
|
|
||||||
CreateTiles();
|
|
||||||
InvalidateVisual();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateTiles()
|
|
||||||
{
|
|
||||||
var tiles = new List<DrawingTile>(TileMatrix.Width * TileMatrix.Height);
|
|
||||||
|
|
||||||
for (var y = TileMatrix.YMin; y <= TileMatrix.YMax; y++)
|
|
||||||
{
|
|
||||||
for (var x = TileMatrix.XMin; x <= TileMatrix.XMax; x++)
|
|
||||||
{
|
|
||||||
var tile = Tiles.FirstOrDefault(t => t.X == x && t.Y == y);
|
|
||||||
|
|
||||||
if (tile == null)
|
|
||||||
{
|
|
||||||
tile = new DrawingTile(TileMatrix.ZoomLevel, x, y, WmtsTileMatrix.MatrixWidth);
|
|
||||||
|
|
||||||
var equivalentTile = Tiles.FirstOrDefault(t => t.ImageDrawing.ImageSource != null && t.Column == tile.Column && t.Row == tile.Row);
|
|
||||||
|
|
||||||
if (equivalentTile != null)
|
|
||||||
{
|
|
||||||
tile.IsPending = false;
|
|
||||||
tile.ImageDrawing.ImageSource = equivalentTile.ImageDrawing.ImageSource; // no Opacity animation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tile.ImageDrawing.Rect = new Rect(
|
|
||||||
WmtsTileMatrix.TileWidth * (x - TileMatrix.XMin),
|
|
||||||
WmtsTileMatrix.TileHeight * (y - TileMatrix.YMin),
|
|
||||||
WmtsTileMatrix.TileWidth,
|
|
||||||
WmtsTileMatrix.TileHeight);
|
|
||||||
|
|
||||||
tiles.Add(tile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Tiles = tiles;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdateRenderTransform(ViewTransform viewTransform)
|
||||||
|
{
|
||||||
|
// Tile matrix origin in pixels.
|
||||||
|
//
|
||||||
|
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
|
||||||
|
|
||||||
|
transform.Matrix = viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UpdateTiles(ViewTransform viewTransform, double viewWidth, double viewHeight)
|
||||||
|
{
|
||||||
|
// Tile matrix bounds in pixels.
|
||||||
|
//
|
||||||
|
var bounds = viewTransform.GetTileMatrixBounds(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, viewWidth, viewHeight);
|
||||||
|
|
||||||
|
// Tile X and Y bounds.
|
||||||
|
//
|
||||||
|
var xMin = (int)Math.Floor(bounds.X / WmtsTileMatrix.TileWidth);
|
||||||
|
var yMin = (int)Math.Floor(bounds.Y / WmtsTileMatrix.TileHeight);
|
||||||
|
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / WmtsTileMatrix.TileWidth);
|
||||||
|
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / WmtsTileMatrix.TileHeight);
|
||||||
|
|
||||||
|
if (!WmtsTileMatrix.HasFullHorizontalCoverage)
|
||||||
|
{
|
||||||
|
// Set X range limits.
|
||||||
|
//
|
||||||
|
xMin = Math.Max(xMin, 0);
|
||||||
|
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Y range limits.
|
||||||
|
//
|
||||||
|
yMin = Math.Max(yMin, 0);
|
||||||
|
yMax = Math.Min(Math.Max(yMax, 0), WmtsTileMatrix.MatrixHeight - 1);
|
||||||
|
|
||||||
|
if (TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
|
||||||
|
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
|
||||||
|
{
|
||||||
|
// No change of the TileMatrix and the Tiles collection.
|
||||||
|
//
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax);
|
||||||
|
|
||||||
|
CreateTiles();
|
||||||
|
InvalidateVisual();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateTiles()
|
||||||
|
{
|
||||||
|
var tiles = new List<DrawingTile>(TileMatrix.Width * TileMatrix.Height);
|
||||||
|
|
||||||
|
for (var y = TileMatrix.YMin; y <= TileMatrix.YMax; y++)
|
||||||
|
{
|
||||||
|
for (var x = TileMatrix.XMin; x <= TileMatrix.XMax; x++)
|
||||||
|
{
|
||||||
|
var tile = Tiles.FirstOrDefault(t => t.X == x && t.Y == y);
|
||||||
|
|
||||||
|
if (tile == null)
|
||||||
|
{
|
||||||
|
tile = new DrawingTile(TileMatrix.ZoomLevel, x, y, WmtsTileMatrix.MatrixWidth);
|
||||||
|
|
||||||
|
var equivalentTile = Tiles.FirstOrDefault(t => t.ImageDrawing.ImageSource != null && t.Column == tile.Column && t.Row == tile.Row);
|
||||||
|
|
||||||
|
if (equivalentTile != null)
|
||||||
|
{
|
||||||
|
tile.IsPending = false;
|
||||||
|
tile.ImageDrawing.ImageSource = equivalentTile.ImageDrawing.ImageSource; // no Opacity animation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tile.ImageDrawing.Rect = new Rect(
|
||||||
|
WmtsTileMatrix.TileWidth * (x - TileMatrix.XMin),
|
||||||
|
WmtsTileMatrix.TileHeight * (y - TileMatrix.YMin),
|
||||||
|
WmtsTileMatrix.TileWidth,
|
||||||
|
WmtsTileMatrix.TileHeight);
|
||||||
|
|
||||||
|
tiles.Add(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Tiles = tiles;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,113 +5,112 @@ using System.Threading.Tasks;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public static partial class GeoImage
|
||||||
{
|
{
|
||||||
public static partial class GeoImage
|
private static Task<GeoBitmap> LoadGeoTiff(string sourcePath)
|
||||||
{
|
{
|
||||||
private static Task<GeoBitmap> LoadGeoTiff(string sourcePath)
|
return Task.Run(() =>
|
||||||
{
|
{
|
||||||
return Task.Run(() =>
|
BitmapSource bitmap;
|
||||||
|
Matrix transform;
|
||||||
|
MapProjection projection = null;
|
||||||
|
|
||||||
|
using (var stream = File.OpenRead(sourcePath))
|
||||||
{
|
{
|
||||||
BitmapSource bitmap;
|
bitmap = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
|
||||||
Matrix transform;
|
}
|
||||||
MapProjection projection = null;
|
|
||||||
|
|
||||||
using (var stream = File.OpenRead(sourcePath))
|
var metadata = (BitmapMetadata)bitmap.Metadata;
|
||||||
{
|
|
||||||
bitmap = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata = (BitmapMetadata)bitmap.Metadata;
|
if (metadata.GetQuery(QueryString(ModelPixelScaleTag)) is double[] pixelScale &&
|
||||||
|
pixelScale.Length == 3 &&
|
||||||
|
metadata.GetQuery(QueryString(ModelTiePointTag)) is double[] tiePoint &&
|
||||||
|
tiePoint.Length >= 6)
|
||||||
|
{
|
||||||
|
transform = new Matrix(pixelScale[0], 0d, 0d, -pixelScale[1], tiePoint[3], tiePoint[4]);
|
||||||
|
}
|
||||||
|
else if (metadata.GetQuery(QueryString(ModelTransformationTag)) is double[] transformValues &&
|
||||||
|
transformValues.Length == 16)
|
||||||
|
{
|
||||||
|
transform = new Matrix(transformValues[0], transformValues[1],
|
||||||
|
transformValues[4], transformValues[5],
|
||||||
|
transformValues[3], transformValues[7]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("No coordinate transformation found.");
|
||||||
|
}
|
||||||
|
|
||||||
if (metadata.GetQuery(QueryString(ModelPixelScaleTag)) is double[] pixelScale &&
|
if (metadata.GetQuery(QueryString(GeoKeyDirectoryTag)) is short[] geoKeyDirectory)
|
||||||
pixelScale.Length == 3 &&
|
{
|
||||||
metadata.GetQuery(QueryString(ModelTiePointTag)) is double[] tiePoint &&
|
projection = GetProjection(geoKeyDirectory);
|
||||||
tiePoint.Length >= 6)
|
}
|
||||||
{
|
|
||||||
transform = new Matrix(pixelScale[0], 0d, 0d, -pixelScale[1], tiePoint[3], tiePoint[4]);
|
|
||||||
}
|
|
||||||
else if (metadata.GetQuery(QueryString(ModelTransformationTag)) is double[] transformValues &&
|
|
||||||
transformValues.Length == 16)
|
|
||||||
{
|
|
||||||
transform = new Matrix(transformValues[0], transformValues[1],
|
|
||||||
transformValues[4], transformValues[5],
|
|
||||||
transformValues[3], transformValues[7]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ArgumentException("No coordinate transformation found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.GetQuery(QueryString(GeoKeyDirectoryTag)) is short[] geoKeyDirectory)
|
if (metadata.GetQuery(QueryString(NoDataTag)) is string noData &&
|
||||||
{
|
int.TryParse(noData, out int noDataValue))
|
||||||
projection = GetProjection(geoKeyDirectory);
|
{
|
||||||
}
|
bitmap = ConvertTransparentPixel(bitmap, noDataValue);
|
||||||
|
}
|
||||||
|
|
||||||
if (metadata.GetQuery(QueryString(NoDataTag)) is string noData &&
|
return new GeoBitmap(bitmap, transform, projection);
|
||||||
int.TryParse(noData, out int noDataValue))
|
});
|
||||||
{
|
}
|
||||||
bitmap = ConvertTransparentPixel(bitmap, noDataValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GeoBitmap(bitmap, transform, projection);
|
private static BitmapSource ConvertTransparentPixel(BitmapSource source, int transparentPixel)
|
||||||
});
|
{
|
||||||
|
BitmapPalette sourcePalette = null;
|
||||||
|
var targetFormat = source.Format;
|
||||||
|
|
||||||
|
if (source.Format == PixelFormats.Indexed8 ||
|
||||||
|
source.Format == PixelFormats.Indexed4 ||
|
||||||
|
source.Format == PixelFormats.Indexed2 ||
|
||||||
|
source.Format == PixelFormats.Indexed1)
|
||||||
|
{
|
||||||
|
sourcePalette = source.Palette;
|
||||||
|
}
|
||||||
|
else if (source.Format == PixelFormats.Gray8)
|
||||||
|
{
|
||||||
|
sourcePalette = BitmapPalettes.Gray256;
|
||||||
|
targetFormat = PixelFormats.Indexed8;
|
||||||
|
}
|
||||||
|
else if (source.Format == PixelFormats.Gray4)
|
||||||
|
{
|
||||||
|
sourcePalette = BitmapPalettes.Gray16;
|
||||||
|
targetFormat = PixelFormats.Indexed4;
|
||||||
|
}
|
||||||
|
else if (source.Format == PixelFormats.Gray2)
|
||||||
|
{
|
||||||
|
sourcePalette = BitmapPalettes.Gray4;
|
||||||
|
targetFormat = PixelFormats.Indexed2;
|
||||||
|
}
|
||||||
|
else if (source.Format == PixelFormats.BlackWhite)
|
||||||
|
{
|
||||||
|
sourcePalette = BitmapPalettes.BlackAndWhite;
|
||||||
|
targetFormat = PixelFormats.Indexed1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BitmapSource ConvertTransparentPixel(BitmapSource source, int transparentPixel)
|
if (sourcePalette == null || transparentPixel >= sourcePalette.Colors.Count)
|
||||||
{
|
{
|
||||||
BitmapPalette sourcePalette = null;
|
return source;
|
||||||
var targetFormat = source.Format;
|
|
||||||
|
|
||||||
if (source.Format == PixelFormats.Indexed8 ||
|
|
||||||
source.Format == PixelFormats.Indexed4 ||
|
|
||||||
source.Format == PixelFormats.Indexed2 ||
|
|
||||||
source.Format == PixelFormats.Indexed1)
|
|
||||||
{
|
|
||||||
sourcePalette = source.Palette;
|
|
||||||
}
|
|
||||||
else if (source.Format == PixelFormats.Gray8)
|
|
||||||
{
|
|
||||||
sourcePalette = BitmapPalettes.Gray256;
|
|
||||||
targetFormat = PixelFormats.Indexed8;
|
|
||||||
}
|
|
||||||
else if (source.Format == PixelFormats.Gray4)
|
|
||||||
{
|
|
||||||
sourcePalette = BitmapPalettes.Gray16;
|
|
||||||
targetFormat = PixelFormats.Indexed4;
|
|
||||||
}
|
|
||||||
else if (source.Format == PixelFormats.Gray2)
|
|
||||||
{
|
|
||||||
sourcePalette = BitmapPalettes.Gray4;
|
|
||||||
targetFormat = PixelFormats.Indexed2;
|
|
||||||
}
|
|
||||||
else if (source.Format == PixelFormats.BlackWhite)
|
|
||||||
{
|
|
||||||
sourcePalette = BitmapPalettes.BlackAndWhite;
|
|
||||||
targetFormat = PixelFormats.Indexed1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourcePalette == null || transparentPixel >= sourcePalette.Colors.Count)
|
|
||||||
{
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
var colors = sourcePalette.Colors.ToList();
|
|
||||||
|
|
||||||
colors[transparentPixel] = Colors.Transparent;
|
|
||||||
|
|
||||||
var stride = (source.PixelWidth * source.Format.BitsPerPixel + 7) / 8;
|
|
||||||
var buffer = new byte[stride * source.PixelHeight];
|
|
||||||
|
|
||||||
source.CopyPixels(buffer, stride, 0);
|
|
||||||
|
|
||||||
var target = BitmapSource.Create(
|
|
||||||
source.PixelWidth, source.PixelHeight, source.DpiX, source.DpiY,
|
|
||||||
targetFormat, new BitmapPalette(colors), buffer, stride);
|
|
||||||
|
|
||||||
target.Freeze();
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var colors = sourcePalette.Colors.ToList();
|
||||||
|
|
||||||
|
colors[transparentPixel] = Colors.Transparent;
|
||||||
|
|
||||||
|
var stride = (source.PixelWidth * source.Format.BitsPerPixel + 7) / 8;
|
||||||
|
var buffer = new byte[stride * source.PixelHeight];
|
||||||
|
|
||||||
|
source.CopyPixels(buffer, stride, 0);
|
||||||
|
|
||||||
|
var target = BitmapSource.Create(
|
||||||
|
source.PixelWidth, source.PixelHeight, source.DpiX, source.DpiY,
|
||||||
|
targetFormat, new BitmapPalette(colors), buffer, stride);
|
||||||
|
|
||||||
|
target.Freeze();
|
||||||
|
|
||||||
|
return target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,92 +6,91 @@ using System.Windows;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public static partial class ImageLoader
|
||||||
{
|
{
|
||||||
public static partial class ImageLoader
|
public static ImageSource LoadResourceImage(Uri uri)
|
||||||
{
|
{
|
||||||
public static ImageSource LoadResourceImage(Uri uri)
|
return new BitmapImage(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImageSource LoadImage(Stream stream)
|
||||||
|
{
|
||||||
|
var image = new BitmapImage();
|
||||||
|
|
||||||
|
image.BeginInit();
|
||||||
|
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||||
|
image.StreamSource = stream;
|
||||||
|
image.EndInit();
|
||||||
|
image.Freeze();
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImageSource LoadImage(string path)
|
||||||
|
{
|
||||||
|
ImageSource image = null;
|
||||||
|
|
||||||
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
return new BitmapImage(uri);
|
using var stream = File.OpenRead(path);
|
||||||
|
|
||||||
|
image = LoadImage(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ImageSource LoadImage(Stream stream)
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<ImageSource> LoadImageAsync(Stream stream)
|
||||||
|
{
|
||||||
|
return Thread.CurrentThread.IsThreadPoolThread ?
|
||||||
|
Task.FromResult(LoadImage(stream)) :
|
||||||
|
Task.Run(() => LoadImage(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<ImageSource> LoadImageAsync(string path)
|
||||||
|
{
|
||||||
|
return Thread.CurrentThread.IsThreadPoolThread ?
|
||||||
|
Task.FromResult(LoadImage(path)) :
|
||||||
|
Task.Run(() => LoadImage(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
|
||||||
|
{
|
||||||
|
WriteableBitmap mergedBitmap = null;
|
||||||
|
var p1 = 0d;
|
||||||
|
var p2 = 0d;
|
||||||
|
|
||||||
|
var images = await Task.WhenAll(
|
||||||
|
LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
|
||||||
|
LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
|
||||||
|
|
||||||
|
if (images.Length == 2 &&
|
||||||
|
images[0] is BitmapSource bitmap1 &&
|
||||||
|
images[1] is BitmapSource bitmap2 &&
|
||||||
|
bitmap1.PixelHeight == bitmap2.PixelHeight &&
|
||||||
|
bitmap1.Format == bitmap2.Format &&
|
||||||
|
bitmap1.Format.BitsPerPixel % 8 == 0)
|
||||||
{
|
{
|
||||||
var image = new BitmapImage();
|
var format = bitmap1.Format;
|
||||||
|
var height = bitmap1.PixelHeight;
|
||||||
|
var width1 = bitmap1.PixelWidth;
|
||||||
|
var width2 = bitmap2.PixelWidth;
|
||||||
|
var stride1 = width1 * format.BitsPerPixel / 8;
|
||||||
|
var stride2 = width2 * format.BitsPerPixel / 8;
|
||||||
|
var buffer1 = new byte[stride1 * height];
|
||||||
|
var buffer2 = new byte[stride2 * height];
|
||||||
|
|
||||||
image.BeginInit();
|
bitmap1.CopyPixels(buffer1, stride1, 0);
|
||||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
bitmap2.CopyPixels(buffer2, stride2, 0);
|
||||||
image.StreamSource = stream;
|
|
||||||
image.EndInit();
|
|
||||||
image.Freeze();
|
|
||||||
|
|
||||||
return image;
|
mergedBitmap = new WriteableBitmap(width1 + width2, height, 96, 96, format, null);
|
||||||
|
mergedBitmap.WritePixels(new Int32Rect(0, 0, width1, height), buffer1, stride1, 0);
|
||||||
|
mergedBitmap.WritePixels(new Int32Rect(width1, 0, width2, height), buffer2, stride2, 0);
|
||||||
|
mergedBitmap.Freeze();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ImageSource LoadImage(string path)
|
return mergedBitmap;
|
||||||
{
|
|
||||||
ImageSource image = null;
|
|
||||||
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
using var stream = File.OpenRead(path);
|
|
||||||
|
|
||||||
image = LoadImage(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<ImageSource> LoadImageAsync(Stream stream)
|
|
||||||
{
|
|
||||||
return Thread.CurrentThread.IsThreadPoolThread ?
|
|
||||||
Task.FromResult(LoadImage(stream)) :
|
|
||||||
Task.Run(() => LoadImage(stream));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<ImageSource> LoadImageAsync(string path)
|
|
||||||
{
|
|
||||||
return Thread.CurrentThread.IsThreadPoolThread ?
|
|
||||||
Task.FromResult(LoadImage(path)) :
|
|
||||||
Task.Run(() => LoadImage(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
|
|
||||||
{
|
|
||||||
WriteableBitmap mergedBitmap = null;
|
|
||||||
var p1 = 0d;
|
|
||||||
var p2 = 0d;
|
|
||||||
|
|
||||||
var images = await Task.WhenAll(
|
|
||||||
LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
|
|
||||||
LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
|
|
||||||
|
|
||||||
if (images.Length == 2 &&
|
|
||||||
images[0] is BitmapSource bitmap1 &&
|
|
||||||
images[1] is BitmapSource bitmap2 &&
|
|
||||||
bitmap1.PixelHeight == bitmap2.PixelHeight &&
|
|
||||||
bitmap1.Format == bitmap2.Format &&
|
|
||||||
bitmap1.Format.BitsPerPixel % 8 == 0)
|
|
||||||
{
|
|
||||||
var format = bitmap1.Format;
|
|
||||||
var height = bitmap1.PixelHeight;
|
|
||||||
var width1 = bitmap1.PixelWidth;
|
|
||||||
var width2 = bitmap2.PixelWidth;
|
|
||||||
var stride1 = width1 * format.BitsPerPixel / 8;
|
|
||||||
var stride2 = width2 * format.BitsPerPixel / 8;
|
|
||||||
var buffer1 = new byte[stride1 * height];
|
|
||||||
var buffer2 = new byte[stride2 * height];
|
|
||||||
|
|
||||||
bitmap1.CopyPixels(buffer1, stride1, 0);
|
|
||||||
bitmap2.CopyPixels(buffer2, stride2, 0);
|
|
||||||
|
|
||||||
mergedBitmap = new WriteableBitmap(width1 + width2, height, 96, 96, format, null);
|
|
||||||
mergedBitmap.WritePixels(new Int32Rect(0, 0, width1, height), buffer1, stride1, 0);
|
|
||||||
mergedBitmap.WritePixels(new Int32Rect(width1, 0, width2, height), buffer2, stride2, 0);
|
|
||||||
mergedBitmap.Freeze();
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergedBitmap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,68 +6,67 @@ using System.Windows.Media;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
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)
|
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
|
||||||
: Tile(zoomLevel, x, y, columnCount)
|
|
||||||
|
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
|
||||||
{
|
{
|
||||||
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
|
var image = await loadImageFunc().ConfigureAwait(false);
|
||||||
|
|
||||||
public override async Task LoadImageAsync(Func<Task<ImageSource>> 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 is BitmapSource bitmap && !bitmap.IsFrozen && bitmap.IsDownloading)
|
||||||
|
|
||||||
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
|
|
||||||
{
|
{
|
||||||
if (image is BitmapSource bitmap && !bitmap.IsFrozen && bitmap.IsDownloading)
|
bitmap.DownloadCompleted += BitmapDownloadCompleted;
|
||||||
{
|
bitmap.DownloadFailed += BitmapDownloadFailed;
|
||||||
bitmap.DownloadCompleted += BitmapDownloadCompleted;
|
}
|
||||||
bitmap.DownloadFailed += BitmapDownloadFailed;
|
else
|
||||||
}
|
{
|
||||||
else
|
BeginFadeInAnimation();
|
||||||
{
|
|
||||||
BeginFadeInAnimation();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Image.Dispatcher.InvokeAsync(SetImageSource);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BeginFadeInAnimation()
|
await Image.Dispatcher.InvokeAsync(SetImageSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginFadeInAnimation()
|
||||||
|
{
|
||||||
|
var fadeInAnimation = new DoubleAnimation
|
||||||
{
|
{
|
||||||
var fadeInAnimation = new DoubleAnimation
|
From = 0d,
|
||||||
{
|
Duration = MapBase.ImageFadeDuration,
|
||||||
From = 0d,
|
FillBehavior = FillBehavior.Stop
|
||||||
Duration = MapBase.ImageFadeDuration,
|
};
|
||||||
FillBehavior = FillBehavior.Stop
|
|
||||||
};
|
|
||||||
|
|
||||||
Image.BeginAnimation(UIElement.OpacityProperty, fadeInAnimation);
|
Image.BeginAnimation(UIElement.OpacityProperty, fadeInAnimation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BitmapDownloadCompleted(object sender, EventArgs e)
|
private void BitmapDownloadCompleted(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var bitmap = (BitmapSource)sender;
|
var bitmap = (BitmapSource)sender;
|
||||||
|
|
||||||
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
|
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
|
||||||
bitmap.DownloadFailed -= BitmapDownloadFailed;
|
bitmap.DownloadFailed -= BitmapDownloadFailed;
|
||||||
|
|
||||||
BeginFadeInAnimation();
|
BeginFadeInAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)
|
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)
|
||||||
{
|
{
|
||||||
var bitmap = (BitmapSource)sender;
|
var bitmap = (BitmapSource)sender;
|
||||||
|
|
||||||
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
|
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
|
||||||
bitmap.DownloadFailed -= BitmapDownloadFailed;
|
bitmap.DownloadFailed -= BitmapDownloadFailed;
|
||||||
|
|
||||||
Image.Source = null;
|
Image.Source = null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,38 +2,37 @@
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public class LocationAnimation : AnimationTimeline
|
||||||
{
|
{
|
||||||
public class LocationAnimation : AnimationTimeline
|
public override Type TargetPropertyType => typeof(Location);
|
||||||
|
|
||||||
|
public Location To { get; set; }
|
||||||
|
|
||||||
|
public IEasingFunction EasingFunction { get; set; }
|
||||||
|
|
||||||
|
protected override Freezable CreateInstanceCore()
|
||||||
{
|
{
|
||||||
public override Type TargetPropertyType => typeof(Location);
|
return new LocationAnimation
|
||||||
|
|
||||||
public Location To { get; set; }
|
|
||||||
|
|
||||||
public IEasingFunction EasingFunction { get; set; }
|
|
||||||
|
|
||||||
protected override Freezable CreateInstanceCore()
|
|
||||||
{
|
{
|
||||||
return new LocationAnimation
|
To = To,
|
||||||
{
|
EasingFunction = EasingFunction
|
||||||
To = To,
|
};
|
||||||
EasingFunction = EasingFunction
|
}
|
||||||
};
|
|
||||||
|
public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock)
|
||||||
|
{
|
||||||
|
var from = (Location)defaultOriginValue;
|
||||||
|
var progress = animationClock.CurrentProgress ?? 1d;
|
||||||
|
|
||||||
|
if (EasingFunction != null)
|
||||||
|
{
|
||||||
|
progress = EasingFunction.Ease(progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock)
|
return new Location(
|
||||||
{
|
(1d - progress) * from.Latitude + progress * To.Latitude,
|
||||||
var from = (Location)defaultOriginValue;
|
(1d - progress) * from.Longitude + progress * To.Longitude);
|
||||||
var progress = animationClock.CurrentProgress ?? 1d;
|
|
||||||
|
|
||||||
if (EasingFunction != null)
|
|
||||||
{
|
|
||||||
progress = EasingFunction.Ease(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Location(
|
|
||||||
(1d - progress) * from.Latitude + progress * To.Latitude,
|
|
||||||
(1d - progress) * from.Longitude + progress * To.Longitude);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,92 @@
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class Map
|
||||||
{
|
{
|
||||||
public partial class Map
|
static Map()
|
||||||
{
|
{
|
||||||
static Map()
|
IsManipulationEnabledProperty.OverrideMetadata(typeof(Map), new FrameworkPropertyMetadata(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ManipulationModeProperty =
|
||||||
|
DependencyPropertyHelper.Register<Map, ManipulationModes>(nameof(ManipulationMode), ManipulationModes.Translate | ManipulationModes.Scale);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value that specifies how the map control handles manipulations.
|
||||||
|
/// </summary>
|
||||||
|
public ManipulationModes ManipulationMode
|
||||||
|
{
|
||||||
|
get => (ManipulationModes)GetValue(ManipulationModeProperty);
|
||||||
|
set => SetValue(ManipulationModeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseWheel(MouseWheelEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnMouseWheel(e);
|
||||||
|
|
||||||
|
// Standard mouse wheel delta value is 120.
|
||||||
|
//
|
||||||
|
OnMouseWheel(e.GetPosition(this), e.Delta / 120d);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Point? mousePosition;
|
||||||
|
|
||||||
|
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPreviewMouseLeftButtonDown(e);
|
||||||
|
|
||||||
|
if (Keyboard.Modifiers == ModifierKeys.None)
|
||||||
{
|
{
|
||||||
IsManipulationEnabledProperty.OverrideMetadata(typeof(Map), new FrameworkPropertyMetadata(true));
|
// Do not call CaptureMouse here because it avoids MapItem selection.
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly DependencyProperty ManipulationModeProperty =
|
|
||||||
DependencyPropertyHelper.Register<Map, ManipulationModes>(nameof(ManipulationMode), ManipulationModes.Translate | ManipulationModes.Scale);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value that specifies how the map control handles manipulations.
|
|
||||||
/// </summary>
|
|
||||||
public ManipulationModes ManipulationMode
|
|
||||||
{
|
|
||||||
get => (ManipulationModes)GetValue(ManipulationModeProperty);
|
|
||||||
set => SetValue(ManipulationModeProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnMouseWheel(MouseWheelEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnMouseWheel(e);
|
|
||||||
|
|
||||||
// Standard mouse wheel delta value is 120.
|
|
||||||
//
|
//
|
||||||
OnMouseWheel(e.GetPosition(this), e.Delta / 120d);
|
mousePosition = e.GetPosition(this);
|
||||||
}
|
|
||||||
|
|
||||||
private Point? mousePosition;
|
|
||||||
|
|
||||||
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnPreviewMouseLeftButtonDown(e);
|
|
||||||
|
|
||||||
if (Keyboard.Modifiers == ModifierKeys.None)
|
|
||||||
{
|
|
||||||
// Do not call CaptureMouse here because it avoids MapItem selection.
|
|
||||||
//
|
|
||||||
mousePosition = e.GetPosition(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnPreviewMouseLeftButtonUp(e);
|
|
||||||
|
|
||||||
if (mousePosition.HasValue)
|
|
||||||
{
|
|
||||||
mousePosition = null;
|
|
||||||
ReleaseMouseCapture();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnMouseMove(MouseEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnMouseMove(e);
|
|
||||||
|
|
||||||
if (mousePosition.HasValue)
|
|
||||||
{
|
|
||||||
if (!IsMouseCaptured)
|
|
||||||
{
|
|
||||||
CaptureMouse();
|
|
||||||
}
|
|
||||||
|
|
||||||
var p = e.GetPosition(this);
|
|
||||||
TranslateMap(new Point(p.X - mousePosition.Value.X, p.Y - mousePosition.Value.Y));
|
|
||||||
mousePosition = p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
|
|
||||||
{
|
|
||||||
Manipulation.SetManipulationMode(this, ManipulationMode);
|
|
||||||
|
|
||||||
base.OnManipulationStarted(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnManipulationDelta(e);
|
|
||||||
|
|
||||||
TransformMap(e.ManipulationOrigin,
|
|
||||||
(Point)e.DeltaManipulation.Translation,
|
|
||||||
e.DeltaManipulation.Rotation,
|
|
||||||
e.DeltaManipulation.Scale.LengthSquared / 2d);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPreviewMouseLeftButtonUp(e);
|
||||||
|
|
||||||
|
if (mousePosition.HasValue)
|
||||||
|
{
|
||||||
|
mousePosition = null;
|
||||||
|
ReleaseMouseCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseMove(MouseEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnMouseMove(e);
|
||||||
|
|
||||||
|
if (mousePosition.HasValue)
|
||||||
|
{
|
||||||
|
if (!IsMouseCaptured)
|
||||||
|
{
|
||||||
|
CaptureMouse();
|
||||||
|
}
|
||||||
|
|
||||||
|
var p = e.GetPosition(this);
|
||||||
|
TranslateMap(new Point(p.X - mousePosition.Value.X, p.Y - mousePosition.Value.Y));
|
||||||
|
mousePosition = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
|
||||||
|
{
|
||||||
|
Manipulation.SetManipulationMode(this, ManipulationMode);
|
||||||
|
|
||||||
|
base.OnManipulationStarted(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnManipulationDelta(e);
|
||||||
|
|
||||||
|
TransformMap(e.ManipulationOrigin,
|
||||||
|
(Point)e.DeltaManipulation.Translation,
|
||||||
|
e.DeltaManipulation.Rotation,
|
||||||
|
e.DeltaManipulation.Scale.LengthSquared / 2d);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,278 +3,277 @@ using System.Windows.Documents;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapBase
|
||||||
{
|
{
|
||||||
public partial class MapBase
|
public static readonly DependencyProperty ForegroundProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapBase, Brush>(TextElement.ForegroundProperty, Brushes.Black);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty AnimationEasingFunctionProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, IEasingFunction>(nameof(AnimationEasingFunction),
|
||||||
|
new QuadraticEase { EasingMode = EasingMode.EaseOut });
|
||||||
|
|
||||||
|
public static readonly DependencyProperty CenterProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, Location>(nameof(Center), new Location(0d, 0d),
|
||||||
|
(map, oldValue, newValue) => map.CenterPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceCenterProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty TargetCenterProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(0d, 0d),
|
||||||
|
(map, oldValue, newValue) => map.TargetCenterPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceCenterProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MinZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d,
|
||||||
|
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceMinZoomLevelProperty(value));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MaxZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d,
|
||||||
|
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceMaxZoomLevelProperty(value));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d,
|
||||||
|
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceZoomLevelProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty TargetZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d,
|
||||||
|
(map, oldValue, newValue) => map.TargetZoomLevelPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceZoomLevelProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty HeadingProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d,
|
||||||
|
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceHeadingProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty TargetHeadingProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d,
|
||||||
|
(map, oldValue, newValue) => map.TargetHeadingPropertyChanged(newValue),
|
||||||
|
(map, value) => map.CoerceHeadingProperty(value),
|
||||||
|
true);
|
||||||
|
|
||||||
|
private static readonly DependencyPropertyKey ViewScalePropertyKey =
|
||||||
|
DependencyPropertyHelper.RegisterReadOnly<MapBase, double>(nameof(ViewScale));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ViewScaleProperty = ViewScalePropertyKey.DependencyProperty;
|
||||||
|
|
||||||
|
private LocationAnimation centerAnimation;
|
||||||
|
private DoubleAnimation zoomLevelAnimation;
|
||||||
|
private DoubleAnimation headingAnimation;
|
||||||
|
|
||||||
|
static MapBase()
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty ForegroundProperty =
|
BackgroundProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(Brushes.White));
|
||||||
DependencyPropertyHelper.AddOwner<MapBase, Brush>(TextElement.ForegroundProperty, Brushes.Black);
|
ClipToBoundsProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(true));
|
||||||
|
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(typeof(MapBase)));
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty AnimationEasingFunctionProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MapBase, IEasingFunction>(nameof(AnimationEasingFunction),
|
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
|
||||||
new QuadraticEase { EasingMode = EasingMode.EaseOut });
|
/// The default value is a QuadraticEase with EasingMode.EaseOut.
|
||||||
|
/// </summary>
|
||||||
|
public IEasingFunction AnimationEasingFunction
|
||||||
|
{
|
||||||
|
get => (IEasingFunction)GetValue(AnimationEasingFunctionProperty);
|
||||||
|
set => SetValue(AnimationEasingFunctionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty CenterProperty =
|
/// <summary>
|
||||||
DependencyPropertyHelper.Register<MapBase, Location>(nameof(Center), new Location(0d, 0d),
|
/// Gets the scaling factor from projected map coordinates to view coordinates,
|
||||||
(map, oldValue, newValue) => map.CenterPropertyChanged(newValue),
|
/// as pixels per meter.
|
||||||
(map, value) => map.CoerceCenterProperty(value),
|
/// </summary>
|
||||||
true);
|
public double ViewScale
|
||||||
|
{
|
||||||
|
get => (double)GetValue(ViewScaleProperty);
|
||||||
|
private set => SetValue(ViewScalePropertyKey, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty TargetCenterProperty =
|
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
|
||||||
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(0d, 0d),
|
{
|
||||||
(map, oldValue, newValue) => map.TargetCenterPropertyChanged(newValue),
|
base.OnRenderSizeChanged(sizeInfo);
|
||||||
(map, value) => map.CoerceCenterProperty(value),
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MinZoomLevelProperty =
|
ResetTransformCenter();
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d,
|
UpdateTransform();
|
||||||
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue),
|
}
|
||||||
(map, value) => map.CoerceMinZoomLevelProperty(value));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MaxZoomLevelProperty =
|
private void CenterPropertyChanged(Location center)
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d,
|
{
|
||||||
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue),
|
if (!internalPropertyChange)
|
||||||
(map, value) => map.CoerceMaxZoomLevelProperty(value));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty ZoomLevelProperty =
|
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d,
|
|
||||||
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue),
|
|
||||||
(map, value) => map.CoerceZoomLevelProperty(value),
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty TargetZoomLevelProperty =
|
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d,
|
|
||||||
(map, oldValue, newValue) => map.TargetZoomLevelPropertyChanged(newValue),
|
|
||||||
(map, value) => map.CoerceZoomLevelProperty(value),
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty HeadingProperty =
|
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d,
|
|
||||||
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue),
|
|
||||||
(map, value) => map.CoerceHeadingProperty(value),
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty TargetHeadingProperty =
|
|
||||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d,
|
|
||||||
(map, oldValue, newValue) => map.TargetHeadingPropertyChanged(newValue),
|
|
||||||
(map, value) => map.CoerceHeadingProperty(value),
|
|
||||||
true);
|
|
||||||
|
|
||||||
private static readonly DependencyPropertyKey ViewScalePropertyKey =
|
|
||||||
DependencyPropertyHelper.RegisterReadOnly<MapBase, double>(nameof(ViewScale));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty ViewScaleProperty = ViewScalePropertyKey.DependencyProperty;
|
|
||||||
|
|
||||||
private LocationAnimation centerAnimation;
|
|
||||||
private DoubleAnimation zoomLevelAnimation;
|
|
||||||
private DoubleAnimation headingAnimation;
|
|
||||||
|
|
||||||
static MapBase()
|
|
||||||
{
|
{
|
||||||
BackgroundProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(Brushes.White));
|
|
||||||
ClipToBoundsProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(true));
|
|
||||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(typeof(MapBase)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
|
|
||||||
/// The default value is a QuadraticEase with EasingMode.EaseOut.
|
|
||||||
/// </summary>
|
|
||||||
public IEasingFunction AnimationEasingFunction
|
|
||||||
{
|
|
||||||
get => (IEasingFunction)GetValue(AnimationEasingFunctionProperty);
|
|
||||||
set => SetValue(AnimationEasingFunctionProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the scaling factor from projected map coordinates to view coordinates,
|
|
||||||
/// as pixels per meter.
|
|
||||||
/// </summary>
|
|
||||||
public double ViewScale
|
|
||||||
{
|
|
||||||
get => (double)GetValue(ViewScaleProperty);
|
|
||||||
private set => SetValue(ViewScalePropertyKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
|
|
||||||
{
|
|
||||||
base.OnRenderSizeChanged(sizeInfo);
|
|
||||||
|
|
||||||
ResetTransformCenter();
|
|
||||||
UpdateTransform();
|
UpdateTransform();
|
||||||
}
|
|
||||||
|
|
||||||
private void CenterPropertyChanged(Location center)
|
if (centerAnimation == null)
|
||||||
{
|
|
||||||
if (!internalPropertyChange)
|
|
||||||
{
|
{
|
||||||
UpdateTransform();
|
SetValueInternal(TargetCenterProperty, center);
|
||||||
|
|
||||||
if (centerAnimation == null)
|
|
||||||
{
|
|
||||||
SetValueInternal(TargetCenterProperty, center);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void TargetCenterPropertyChanged(Location targetCenter)
|
private void TargetCenterPropertyChanged(Location targetCenter)
|
||||||
|
{
|
||||||
|
if (!internalPropertyChange && !targetCenter.Equals(Center))
|
||||||
{
|
{
|
||||||
if (!internalPropertyChange && !targetCenter.Equals(Center))
|
ResetTransformCenter();
|
||||||
{
|
|
||||||
ResetTransformCenter();
|
|
||||||
|
|
||||||
if (centerAnimation != null)
|
|
||||||
{
|
|
||||||
centerAnimation.Completed -= CenterAnimationCompleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
centerAnimation = new LocationAnimation
|
|
||||||
{
|
|
||||||
To = new Location(targetCenter.Latitude, NearestLongitude(targetCenter.Longitude)),
|
|
||||||
Duration = AnimationDuration,
|
|
||||||
EasingFunction = AnimationEasingFunction
|
|
||||||
};
|
|
||||||
|
|
||||||
centerAnimation.Completed += CenterAnimationCompleted;
|
|
||||||
|
|
||||||
BeginAnimation(CenterProperty, centerAnimation, HandoffBehavior.Compose);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CenterAnimationCompleted(object sender, object e)
|
|
||||||
{
|
|
||||||
if (centerAnimation != null)
|
if (centerAnimation != null)
|
||||||
{
|
{
|
||||||
centerAnimation.Completed -= CenterAnimationCompleted;
|
centerAnimation.Completed -= CenterAnimationCompleted;
|
||||||
centerAnimation = null;
|
|
||||||
|
|
||||||
SetValueInternal(CenterProperty, TargetCenter);
|
|
||||||
UpdateTransform();
|
|
||||||
BeginAnimation(CenterProperty, null);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void MinZoomLevelPropertyChanged(double minZoomLevel)
|
centerAnimation = new LocationAnimation
|
||||||
{
|
|
||||||
if (ZoomLevel < minZoomLevel)
|
|
||||||
{
|
{
|
||||||
ZoomLevel = minZoomLevel;
|
To = new Location(targetCenter.Latitude, NearestLongitude(targetCenter.Longitude)),
|
||||||
}
|
Duration = AnimationDuration,
|
||||||
}
|
EasingFunction = AnimationEasingFunction
|
||||||
|
};
|
||||||
|
|
||||||
private void MaxZoomLevelPropertyChanged(double maxZoomLevel)
|
centerAnimation.Completed += CenterAnimationCompleted;
|
||||||
|
|
||||||
|
BeginAnimation(CenterProperty, centerAnimation, HandoffBehavior.Compose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CenterAnimationCompleted(object sender, object e)
|
||||||
|
{
|
||||||
|
if (centerAnimation != null)
|
||||||
{
|
{
|
||||||
if (ZoomLevel > maxZoomLevel)
|
centerAnimation.Completed -= CenterAnimationCompleted;
|
||||||
{
|
centerAnimation = null;
|
||||||
ZoomLevel = maxZoomLevel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ZoomLevelPropertyChanged(double zoomLevel)
|
SetValueInternal(CenterProperty, TargetCenter);
|
||||||
|
UpdateTransform();
|
||||||
|
BeginAnimation(CenterProperty, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MinZoomLevelPropertyChanged(double minZoomLevel)
|
||||||
|
{
|
||||||
|
if (ZoomLevel < minZoomLevel)
|
||||||
{
|
{
|
||||||
if (!internalPropertyChange)
|
ZoomLevel = minZoomLevel;
|
||||||
{
|
|
||||||
UpdateTransform();
|
|
||||||
|
|
||||||
if (zoomLevelAnimation == null)
|
|
||||||
{
|
|
||||||
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void TargetZoomLevelPropertyChanged(double targetZoomLevel)
|
private void MaxZoomLevelPropertyChanged(double maxZoomLevel)
|
||||||
|
{
|
||||||
|
if (ZoomLevel > maxZoomLevel)
|
||||||
{
|
{
|
||||||
if (!internalPropertyChange && targetZoomLevel != ZoomLevel)
|
ZoomLevel = maxZoomLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ZoomLevelPropertyChanged(double zoomLevel)
|
||||||
|
{
|
||||||
|
if (!internalPropertyChange)
|
||||||
|
{
|
||||||
|
UpdateTransform();
|
||||||
|
|
||||||
|
if (zoomLevelAnimation == null)
|
||||||
{
|
{
|
||||||
if (zoomLevelAnimation != null)
|
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
||||||
{
|
|
||||||
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
zoomLevelAnimation = new DoubleAnimation
|
|
||||||
{
|
|
||||||
To = targetZoomLevel,
|
|
||||||
Duration = AnimationDuration,
|
|
||||||
EasingFunction = AnimationEasingFunction
|
|
||||||
};
|
|
||||||
|
|
||||||
zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted;
|
|
||||||
|
|
||||||
BeginAnimation(ZoomLevelProperty, zoomLevelAnimation, HandoffBehavior.Compose);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ZoomLevelAnimationCompleted(object sender, object e)
|
private void TargetZoomLevelPropertyChanged(double targetZoomLevel)
|
||||||
|
{
|
||||||
|
if (!internalPropertyChange && targetZoomLevel != ZoomLevel)
|
||||||
{
|
{
|
||||||
if (zoomLevelAnimation != null)
|
if (zoomLevelAnimation != null)
|
||||||
{
|
{
|
||||||
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
|
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
|
||||||
zoomLevelAnimation = null;
|
|
||||||
|
|
||||||
SetValueInternal(ZoomLevelProperty, TargetZoomLevel);
|
|
||||||
UpdateTransform(true);
|
|
||||||
BeginAnimation(ZoomLevelProperty, null);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void HeadingPropertyChanged(double heading)
|
zoomLevelAnimation = new DoubleAnimation
|
||||||
{
|
|
||||||
if (!internalPropertyChange)
|
|
||||||
{
|
{
|
||||||
UpdateTransform();
|
To = targetZoomLevel,
|
||||||
|
Duration = AnimationDuration,
|
||||||
|
EasingFunction = AnimationEasingFunction
|
||||||
|
};
|
||||||
|
|
||||||
if (headingAnimation == null)
|
zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted;
|
||||||
{
|
|
||||||
SetValueInternal(TargetHeadingProperty, heading);
|
BeginAnimation(ZoomLevelProperty, zoomLevelAnimation, HandoffBehavior.Compose);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void TargetHeadingPropertyChanged(double targetHeading)
|
private void ZoomLevelAnimationCompleted(object sender, object e)
|
||||||
|
{
|
||||||
|
if (zoomLevelAnimation != null)
|
||||||
{
|
{
|
||||||
if (!internalPropertyChange && targetHeading != Heading)
|
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
|
||||||
{
|
zoomLevelAnimation = null;
|
||||||
var delta = targetHeading - Heading;
|
|
||||||
|
|
||||||
if (delta > 180d)
|
SetValueInternal(ZoomLevelProperty, TargetZoomLevel);
|
||||||
{
|
UpdateTransform(true);
|
||||||
delta -= 360d;
|
BeginAnimation(ZoomLevelProperty, null);
|
||||||
}
|
|
||||||
else if (delta < -180d)
|
|
||||||
{
|
|
||||||
delta += 360d;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (headingAnimation != null)
|
|
||||||
{
|
|
||||||
headingAnimation.Completed -= HeadingAnimationCompleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
headingAnimation = new DoubleAnimation
|
|
||||||
{
|
|
||||||
By = delta,
|
|
||||||
Duration = AnimationDuration,
|
|
||||||
EasingFunction = AnimationEasingFunction
|
|
||||||
};
|
|
||||||
|
|
||||||
headingAnimation.Completed += HeadingAnimationCompleted;
|
|
||||||
|
|
||||||
BeginAnimation(HeadingProperty, headingAnimation, HandoffBehavior.SnapshotAndReplace); // don't compose
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void HeadingAnimationCompleted(object sender, object e)
|
private void HeadingPropertyChanged(double heading)
|
||||||
|
{
|
||||||
|
if (!internalPropertyChange)
|
||||||
{
|
{
|
||||||
if (headingAnimation != null)
|
UpdateTransform();
|
||||||
{
|
|
||||||
headingAnimation.Completed -= HeadingAnimationCompleted;
|
|
||||||
headingAnimation = null;
|
|
||||||
|
|
||||||
SetValueInternal(HeadingProperty, TargetHeading);
|
if (headingAnimation == null)
|
||||||
UpdateTransform();
|
{
|
||||||
BeginAnimation(HeadingProperty, null);
|
SetValueInternal(TargetHeadingProperty, heading);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TargetHeadingPropertyChanged(double targetHeading)
|
||||||
|
{
|
||||||
|
if (!internalPropertyChange && targetHeading != Heading)
|
||||||
|
{
|
||||||
|
var delta = targetHeading - Heading;
|
||||||
|
|
||||||
|
if (delta > 180d)
|
||||||
|
{
|
||||||
|
delta -= 360d;
|
||||||
|
}
|
||||||
|
else if (delta < -180d)
|
||||||
|
{
|
||||||
|
delta += 360d;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headingAnimation != null)
|
||||||
|
{
|
||||||
|
headingAnimation.Completed -= HeadingAnimationCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
headingAnimation = new DoubleAnimation
|
||||||
|
{
|
||||||
|
By = delta,
|
||||||
|
Duration = AnimationDuration,
|
||||||
|
EasingFunction = AnimationEasingFunction
|
||||||
|
};
|
||||||
|
|
||||||
|
headingAnimation.Completed += HeadingAnimationCompleted;
|
||||||
|
|
||||||
|
BeginAnimation(HeadingProperty, headingAnimation, HandoffBehavior.SnapshotAndReplace); // don't compose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HeadingAnimationCompleted(object sender, object e)
|
||||||
|
{
|
||||||
|
if (headingAnimation != null)
|
||||||
|
{
|
||||||
|
headingAnimation.Completed -= HeadingAnimationCompleted;
|
||||||
|
headingAnimation = null;
|
||||||
|
|
||||||
|
SetValueInternal(HeadingProperty, TargetHeading);
|
||||||
|
UpdateTransform();
|
||||||
|
BeginAnimation(HeadingProperty, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,28 @@
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapContentControl
|
||||||
{
|
{
|
||||||
public partial class MapContentControl
|
static MapContentControl()
|
||||||
{
|
{
|
||||||
static MapContentControl()
|
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapContentControl), new FrameworkPropertyMetadata(typeof(MapContentControl)));
|
||||||
{
|
}
|
||||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapContentControl), new FrameworkPropertyMetadata(typeof(MapContentControl)));
|
}
|
||||||
}
|
|
||||||
}
|
public partial class Pushpin
|
||||||
|
{
|
||||||
public partial class Pushpin
|
static Pushpin()
|
||||||
{
|
{
|
||||||
static Pushpin()
|
DefaultStyleKeyProperty.OverrideMetadata(typeof(Pushpin), new FrameworkPropertyMetadata(typeof(Pushpin)));
|
||||||
{
|
}
|
||||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(Pushpin), new FrameworkPropertyMetadata(typeof(Pushpin)));
|
|
||||||
}
|
public static readonly DependencyProperty CornerRadiusProperty =
|
||||||
|
DependencyPropertyHelper.Register<Pushpin, CornerRadius>(nameof(CornerRadius));
|
||||||
public static readonly DependencyProperty CornerRadiusProperty =
|
|
||||||
DependencyPropertyHelper.Register<Pushpin, CornerRadius>(nameof(CornerRadius));
|
public CornerRadius CornerRadius
|
||||||
|
{
|
||||||
public CornerRadius CornerRadius
|
get => (CornerRadius)GetValue(CornerRadiusProperty);
|
||||||
{
|
set => SetValue(CornerRadiusProperty, value);
|
||||||
get => (CornerRadius)GetValue(CornerRadiusProperty);
|
|
||||||
set => SetValue(CornerRadiusProperty, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,109 +4,108 @@ using System.Windows;
|
||||||
using System.Windows.Documents;
|
using System.Windows.Documents;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapGrid : FrameworkElement, IMapElement
|
||||||
{
|
{
|
||||||
public partial class MapGrid : FrameworkElement, IMapElement
|
public static readonly DependencyProperty ForegroundProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapGrid, Brush>(TextElement.ForegroundProperty);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty FontFamilyProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapGrid, FontFamily>(TextElement.FontFamilyProperty);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty FontSizeProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapGrid, double>(TextElement.FontSizeProperty, 12d);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements IMapElement.ParentMap.
|
||||||
|
/// </summary>
|
||||||
|
public MapBase ParentMap
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty ForegroundProperty =
|
get;
|
||||||
DependencyPropertyHelper.AddOwner<MapGrid, Brush>(TextElement.ForegroundProperty);
|
set
|
||||||
|
|
||||||
public static readonly DependencyProperty FontFamilyProperty =
|
|
||||||
DependencyPropertyHelper.AddOwner<MapGrid, FontFamily>(TextElement.FontFamilyProperty);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty FontSizeProperty =
|
|
||||||
DependencyPropertyHelper.AddOwner<MapGrid, double>(TextElement.FontSizeProperty, 12d);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implements IMapElement.ParentMap.
|
|
||||||
/// </summary>
|
|
||||||
public MapBase ParentMap
|
|
||||||
{
|
{
|
||||||
get;
|
if (field != null)
|
||||||
set
|
|
||||||
{
|
{
|
||||||
if (field != null)
|
field.ViewportChanged -= OnViewportChanged;
|
||||||
{
|
}
|
||||||
field.ViewportChanged -= OnViewportChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
field = value;
|
field = value;
|
||||||
|
|
||||||
if (field != null)
|
if (field != null)
|
||||||
{
|
{
|
||||||
field.ViewportChanged += OnViewportChanged;
|
field.ViewportChanged += OnViewportChanged;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
||||||
{
|
{
|
||||||
OnViewportChanged(e);
|
OnViewportChanged(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
|
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
|
||||||
{
|
{
|
||||||
InvalidateVisual();
|
InvalidateVisual();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnRender(DrawingContext drawingContext)
|
protected override void OnRender(DrawingContext drawingContext)
|
||||||
|
{
|
||||||
|
if (ParentMap != null)
|
||||||
{
|
{
|
||||||
if (ParentMap != null)
|
var pathGeometry = new PathGeometry();
|
||||||
|
var labels = new List<Label>();
|
||||||
|
var pen = new Pen
|
||||||
{
|
{
|
||||||
var pathGeometry = new PathGeometry();
|
Brush = Foreground,
|
||||||
var labels = new List<Label>();
|
Thickness = StrokeThickness,
|
||||||
var pen = new Pen
|
};
|
||||||
|
|
||||||
|
DrawGrid(pathGeometry.Figures, labels);
|
||||||
|
|
||||||
|
drawingContext.DrawGeometry(null, pen, pathGeometry);
|
||||||
|
|
||||||
|
if (labels.Count > 0)
|
||||||
|
{
|
||||||
|
var typeface = new Typeface(FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
|
||||||
|
var pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
|
||||||
|
|
||||||
|
foreach (var label in labels)
|
||||||
{
|
{
|
||||||
Brush = Foreground,
|
var text = new FormattedText(label.Text,
|
||||||
Thickness = StrokeThickness,
|
CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, FontSize, Foreground, pixelsPerDip);
|
||||||
};
|
var x = label.X +
|
||||||
|
label.HorizontalAlignment switch
|
||||||
|
{
|
||||||
|
HorizontalAlignment.Left => 2d,
|
||||||
|
HorizontalAlignment.Right => -text.Width - 2d,
|
||||||
|
_ => -text.Width / 2d
|
||||||
|
};
|
||||||
|
var y = label.Y +
|
||||||
|
label.VerticalAlignment switch
|
||||||
|
{
|
||||||
|
VerticalAlignment.Top => 0,
|
||||||
|
VerticalAlignment.Bottom => -text.Height,
|
||||||
|
_ => -text.Height / 2d
|
||||||
|
};
|
||||||
|
|
||||||
DrawGrid(pathGeometry.Figures, labels);
|
if (label.Rotation != 0d)
|
||||||
|
|
||||||
drawingContext.DrawGeometry(null, pen, pathGeometry);
|
|
||||||
|
|
||||||
if (labels.Count > 0)
|
|
||||||
{
|
|
||||||
var typeface = new Typeface(FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
|
|
||||||
var pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
|
|
||||||
|
|
||||||
foreach (var label in labels)
|
|
||||||
{
|
{
|
||||||
var text = new FormattedText(label.Text,
|
drawingContext.PushTransform(new RotateTransform(label.Rotation, label.X, label.Y));
|
||||||
CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, FontSize, Foreground, pixelsPerDip);
|
drawingContext.DrawText(text, new Point(x, y));
|
||||||
var x = label.X +
|
drawingContext.Pop();
|
||||||
label.HorizontalAlignment switch
|
}
|
||||||
{
|
else
|
||||||
HorizontalAlignment.Left => 2d,
|
{
|
||||||
HorizontalAlignment.Right => -text.Width - 2d,
|
drawingContext.DrawText(text, new Point(x, y));
|
||||||
_ => -text.Width / 2d
|
|
||||||
};
|
|
||||||
var y = label.Y +
|
|
||||||
label.VerticalAlignment switch
|
|
||||||
{
|
|
||||||
VerticalAlignment.Top => 0,
|
|
||||||
VerticalAlignment.Bottom => -text.Height,
|
|
||||||
_ => -text.Height / 2d
|
|
||||||
};
|
|
||||||
|
|
||||||
if (label.Rotation != 0d)
|
|
||||||
{
|
|
||||||
drawingContext.PushTransform(new RotateTransform(label.Rotation, label.X, label.Y));
|
|
||||||
drawingContext.DrawText(text, new Point(x, y));
|
|
||||||
drawingContext.Pop();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
drawingContext.DrawText(text, new Point(x, y));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
|
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
|
||||||
{
|
{
|
||||||
return new PolyLineSegment(points, true);
|
return new PolyLineSegment(points, true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,34 @@
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapImageLayer
|
||||||
{
|
{
|
||||||
public partial class MapImageLayer
|
private void FadeOver()
|
||||||
{
|
{
|
||||||
private void FadeOver()
|
var fadeInAnimation = new DoubleAnimation
|
||||||
{
|
{
|
||||||
var fadeInAnimation = new DoubleAnimation
|
To = 1d,
|
||||||
{
|
Duration = MapBase.ImageFadeDuration
|
||||||
To = 1d,
|
};
|
||||||
Duration = MapBase.ImageFadeDuration
|
|
||||||
};
|
|
||||||
|
|
||||||
var fadeOutAnimation = new DoubleAnimation
|
var fadeOutAnimation = new DoubleAnimation
|
||||||
{
|
{
|
||||||
To = 0d,
|
To = 0d,
|
||||||
BeginTime = MapBase.ImageFadeDuration,
|
BeginTime = MapBase.ImageFadeDuration,
|
||||||
Duration = TimeSpan.Zero
|
Duration = TimeSpan.Zero
|
||||||
};
|
};
|
||||||
|
|
||||||
Storyboard.SetTarget(fadeInAnimation, Children[1]);
|
Storyboard.SetTarget(fadeInAnimation, Children[1]);
|
||||||
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(OpacityProperty));
|
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(OpacityProperty));
|
||||||
|
|
||||||
Storyboard.SetTarget(fadeOutAnimation, Children[0]);
|
Storyboard.SetTarget(fadeOutAnimation, Children[0]);
|
||||||
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(OpacityProperty));
|
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(OpacityProperty));
|
||||||
|
|
||||||
var storyboard = new Storyboard();
|
var storyboard = new Storyboard();
|
||||||
storyboard.Children.Add(fadeInAnimation);
|
storyboard.Children.Add(fadeInAnimation);
|
||||||
storyboard.Children.Add(fadeOutAnimation);
|
storyboard.Children.Add(fadeOutAnimation);
|
||||||
storyboard.Begin();
|
storyboard.Begin();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,38 +2,37 @@
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapItem
|
||||||
{
|
{
|
||||||
public partial class MapItem
|
static MapItem()
|
||||||
{
|
{
|
||||||
static MapItem()
|
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItem), new FrameworkPropertyMetadata(typeof(MapItem)));
|
||||||
{
|
}
|
||||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItem), new FrameworkPropertyMetadata(typeof(MapItem)));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnTouchDown(TouchEventArgs e)
|
protected override void OnTouchDown(TouchEventArgs e)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnTouchUp(TouchEventArgs e)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) &&
|
||||||
|
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl &&
|
||||||
|
mapItemsControl.SelectionMode == SelectionMode.Extended)
|
||||||
{
|
{
|
||||||
|
mapItemsControl.SelectItemsInRange(this);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
protected override void OnTouchUp(TouchEventArgs e)
|
|
||||||
{
|
{
|
||||||
e.Handled = true;
|
base.OnMouseLeftButtonDown(e);
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) &&
|
|
||||||
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl &&
|
|
||||||
mapItemsControl.SelectionMode == SelectionMode.Extended)
|
|
||||||
{
|
|
||||||
mapItemsControl.SelectItemsInRange(this);
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
base.OnMouseLeftButtonDown(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,50 +2,49 @@
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapItemsControl
|
||||||
{
|
{
|
||||||
public partial class MapItemsControl
|
static MapItemsControl()
|
||||||
{
|
{
|
||||||
static MapItemsControl()
|
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItemsControl), new FrameworkPropertyMetadata(typeof(MapItemsControl)));
|
||||||
{
|
}
|
||||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItemsControl), new FrameworkPropertyMetadata(typeof(MapItemsControl)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SelectItemsInGeometry(Geometry geometry)
|
public void SelectItemsInGeometry(Geometry geometry)
|
||||||
{
|
{
|
||||||
SelectItemsByPosition(geometry.FillContains);
|
SelectItemsByPosition(geometry.FillContains);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MapItem ContainerFromItem(object item)
|
public MapItem ContainerFromItem(object item)
|
||||||
{
|
{
|
||||||
return (MapItem)ItemContainerGenerator.ContainerFromItem(item);
|
return (MapItem)ItemContainerGenerator.ContainerFromItem(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public object ItemFromContainer(MapItem container)
|
public object ItemFromContainer(MapItem container)
|
||||||
{
|
{
|
||||||
return ItemContainerGenerator.ItemFromContainer(container);
|
return ItemContainerGenerator.ItemFromContainer(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool IsItemItsOwnContainerOverride(object item)
|
protected override bool IsItemItsOwnContainerOverride(object item)
|
||||||
{
|
{
|
||||||
return item is MapItem;
|
return item is MapItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DependencyObject GetContainerForItemOverride()
|
protected override DependencyObject GetContainerForItemOverride()
|
||||||
{
|
{
|
||||||
return new MapItem();
|
return new MapItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PrepareContainerForItemOverride(DependencyObject container, object item)
|
protected override void PrepareContainerForItemOverride(DependencyObject container, object item)
|
||||||
{
|
{
|
||||||
base.PrepareContainerForItemOverride(container, item);
|
base.PrepareContainerForItemOverride(container, item);
|
||||||
PrepareContainer((MapItem)container, item);
|
PrepareContainer((MapItem)container, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ClearContainerForItemOverride(DependencyObject container, object item)
|
protected override void ClearContainerForItemOverride(DependencyObject container, object item)
|
||||||
{
|
{
|
||||||
base.ClearContainerForItemOverride(container, item);
|
base.ClearContainerForItemOverride(container, item);
|
||||||
ClearContainer((MapItem)container);
|
ClearContainer((MapItem)container);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,26 @@
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapPanel
|
||||||
{
|
{
|
||||||
public partial class MapPanel
|
public static readonly DependencyProperty AutoCollapseProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached< bool>("AutoCollapse", typeof(MapPanel));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty LocationProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<Location>("Location", typeof(MapPanel), null,
|
||||||
|
FrameworkPropertyMetadataOptions.AffectsParentArrange);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty BoundingBoxProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel), null,
|
||||||
|
FrameworkPropertyMetadataOptions.AffectsParentArrange);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MapRectProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<Rect?>("MapRect", typeof(MapPanel), null,
|
||||||
|
FrameworkPropertyMetadataOptions.AffectsParentArrange);
|
||||||
|
|
||||||
|
public static MapBase GetParentMap(FrameworkElement element)
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty AutoCollapseProperty =
|
return (MapBase)element.GetValue(ParentMapProperty);
|
||||||
DependencyPropertyHelper.RegisterAttached< bool>("AutoCollapse", typeof(MapPanel));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty LocationProperty =
|
|
||||||
DependencyPropertyHelper.RegisterAttached<Location>("Location", typeof(MapPanel), null,
|
|
||||||
FrameworkPropertyMetadataOptions.AffectsParentArrange);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty BoundingBoxProperty =
|
|
||||||
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel), null,
|
|
||||||
FrameworkPropertyMetadataOptions.AffectsParentArrange);
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MapRectProperty =
|
|
||||||
DependencyPropertyHelper.RegisterAttached<Rect?>("MapRect", typeof(MapPanel), null,
|
|
||||||
FrameworkPropertyMetadataOptions.AffectsParentArrange);
|
|
||||||
|
|
||||||
public static MapBase GetParentMap(FrameworkElement element)
|
|
||||||
{
|
|
||||||
return (MapBase)element.GetValue(ParentMapProperty);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,36 +2,35 @@
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl;
|
||||||
|
|
||||||
|
public partial class MapPath : Shape
|
||||||
{
|
{
|
||||||
public partial class MapPath : Shape
|
public static readonly DependencyProperty DataProperty =
|
||||||
|
DependencyPropertyHelper.AddOwner<MapPath, Geometry>(Path.DataProperty,
|
||||||
|
(path, oldValue, newValue) => path.DataPropertyChanged(oldValue, newValue));
|
||||||
|
|
||||||
|
public Geometry Data
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty DataProperty =
|
get => (Geometry)GetValue(DataProperty);
|
||||||
DependencyPropertyHelper.AddOwner<MapPath, Geometry>(Path.DataProperty,
|
set => SetValue(DataProperty, value);
|
||||||
(path, oldValue, newValue) => path.DataPropertyChanged(oldValue, newValue));
|
}
|
||||||
|
|
||||||
public Geometry Data
|
protected override Geometry DefiningGeometry => Data;
|
||||||
|
|
||||||
|
private void DataPropertyChanged(Geometry oldValue, Geometry newValue)
|
||||||
|
{
|
||||||
|
// Check if Data is actually a new Geometry.
|
||||||
|
//
|
||||||
|
if (newValue != null && !ReferenceEquals(newValue, oldValue))
|
||||||
{
|
{
|
||||||
get => (Geometry)GetValue(DataProperty);
|
if (newValue.IsFrozen)
|
||||||
set => SetValue(DataProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Geometry DefiningGeometry => Data;
|
|
||||||
|
|
||||||
private void DataPropertyChanged(Geometry oldValue, Geometry newValue)
|
|
||||||
{
|
|
||||||
// Check if Data is actually a new Geometry.
|
|
||||||
//
|
|
||||||
if (newValue != null && !ReferenceEquals(newValue, oldValue))
|
|
||||||
{
|
{
|
||||||
if (newValue.IsFrozen)
|
Data = newValue.Clone(); // DataPropertyChanged called again
|
||||||
{
|
}
|
||||||
Data = newValue.Clone(); // DataPropertyChanged called again
|
else
|
||||||
}
|
{
|
||||||
else
|
UpdateData();
|
||||||
{
|
|
||||||
UpdateData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue