mirror of
https://github.com/ClemensFischer/XAML-Map-Control.git
synced 2026-05-07 13:37:47 +00:00
File scoped namespaces
This commit is contained in:
parent
c14377f976
commit
65aba44af6
152 changed files with 11962 additions and 12115 deletions
|
|
@ -1,50 +1,49 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MapControl
|
||||
{
|
||||
/// <summary>
|
||||
/// A geographic bounding box with south and north latitude and west and east longitude values in degrees.
|
||||
/// </summary>
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// A geographic bounding box with south and north latitude and west and east longitude values in degrees.
|
||||
/// </summary>
|
||||
#if UWP || WINUI
|
||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||
#else
|
||||
[System.ComponentModel.TypeConverter(typeof(BoundingBoxConverter))]
|
||||
[System.ComponentModel.TypeConverter(typeof(BoundingBoxConverter))]
|
||||
#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);
|
||||
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);
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3}", South, West, North, East);
|
||||
}
|
||||
|
||||
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>
|
||||
/// Creates a BoundingBox instance from a string containing a comma-separated sequence of four floating point numbers.
|
||||
/// </summary>
|
||||
public static BoundingBox Parse(string boundingBox)
|
||||
if (values == null || values.Length != 4 && values.Length != 5)
|
||||
{
|
||||
string[] values = null;
|
||||
|
||||
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));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,45 +6,44 @@ using System.Windows.Media;
|
|||
using Avalonia;
|
||||
#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>
|
||||
/// 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
|
||||
public const string DefaultCrsId = "EPSG:4326";
|
||||
|
||||
public EquirectangularProjection() // parameterless constructor for XAML
|
||||
: this(DefaultCrsId)
|
||||
{
|
||||
public const string DefaultCrsId = "EPSG:4326";
|
||||
}
|
||||
|
||||
public EquirectangularProjection() // parameterless constructor for XAML
|
||||
: this(DefaultCrsId)
|
||||
{
|
||||
}
|
||||
public EquirectangularProjection(string crsId)
|
||||
{
|
||||
IsNormalCylindrical = true;
|
||||
CrsId = crsId;
|
||||
}
|
||||
|
||||
public EquirectangularProjection(string crsId)
|
||||
{
|
||||
IsNormalCylindrical = true;
|
||||
CrsId = crsId;
|
||||
}
|
||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||
{
|
||||
return new Matrix(1d / Math.Cos(latitude * Math.PI / 180d), 0d, 0d, 1d, 0d, 0d);
|
||||
}
|
||||
|
||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||
{
|
||||
return new Matrix(1d / Math.Cos(latitude * Math.PI / 180d), 0d, 0d, 1d, 0d, 0d);
|
||||
}
|
||||
public override Point LocationToMap(double latitude, double longitude)
|
||||
{
|
||||
return new Point(
|
||||
EquatorialRadius * Math.PI / 180d * longitude,
|
||||
EquatorialRadius * Math.PI / 180d * latitude);
|
||||
}
|
||||
|
||||
public override Point LocationToMap(double latitude, double longitude)
|
||||
{
|
||||
return new Point(
|
||||
EquatorialRadius * Math.PI / 180d * longitude,
|
||||
EquatorialRadius * Math.PI / 180d * latitude);
|
||||
}
|
||||
|
||||
public override Location MapToLocation(double x, double y)
|
||||
{
|
||||
return new Location(
|
||||
y / EquatorialRadius * 180d / Math.PI,
|
||||
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;
|
||||
|
||||
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
|
||||
return Path.GetFullPath(path);
|
||||
return Path.GetFullPath(path);
|
||||
#else
|
||||
return Path.GetFullPath(path, System.AppDomain.CurrentDomain.BaseDirectory);
|
||||
return Path.GetFullPath(path, System.AppDomain.CurrentDomain.BaseDirectory);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,183 +29,182 @@ using Shape = Avalonia.Controls.Shapes.Shape;
|
|||
using BitmapSource = Avalonia.Media.Imaging.Bitmap;
|
||||
#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
|
||||
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
|
||||
var p2 = transform.Transform(new Point(bitmap.PixelWidth, bitmap.PixelHeight));
|
||||
var p2 = transform.Transform(new Point(bitmap.PixelWidth, bitmap.PixelHeight));
|
||||
#endif
|
||||
BitmapSource = bitmap;
|
||||
BitmapSource = bitmap;
|
||||
|
||||
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))
|
||||
if (projection != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var geoBitmap = await LoadGeoBitmap(sourcePath);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
image.Source = geoBitmap.BitmapSource;
|
||||
}
|
||||
else if (element is Shape shape)
|
||||
{
|
||||
shape.Stretch = Stretch.Fill;
|
||||
shape.Fill = new ImageBrush
|
||||
{
|
||||
Stretch = Stretch.Fill,
|
||||
Stretch = Stretch.Fill,
|
||||
#if AVALONIA
|
||||
Source = geoBitmap.BitmapSource
|
||||
Source = geoBitmap.BitmapSource
|
||||
#else
|
||||
ImageSource = geoBitmap.BitmapSource
|
||||
ImageSource = geoBitmap.BitmapSource
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
MapPanel.SetBoundingBox(element, geoBitmap.BoundingBox);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "Failed loading from {path}", sourcePath);
|
||||
};
|
||||
}
|
||||
|
||||
MapPanel.SetBoundingBox(element, geoBitmap.BoundingBox);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<GeoBitmap> LoadGeoBitmap(string sourcePath)
|
||||
{
|
||||
var ext = System.IO.Path.GetExtension(sourcePath);
|
||||
|
||||
if (ext.Length >= 4)
|
||||
catch (Exception ex)
|
||||
{
|
||||
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);
|
||||
}
|
||||
Logger?.LogError(ex, "Failed loading from {path}", sourcePath);
|
||||
}
|
||||
|
||||
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;
|
||||
#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;
|
||||
SetBoundingBox(Image, latLonBox);
|
||||
Image.SetValue(Canvas.ZIndexProperty, zIndex);
|
||||
}
|
||||
using var memoryStream = new MemoryStream((int)entry.Length);
|
||||
|
||||
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)
|
||||
using (var zipStream = entry.Open())
|
||||
{
|
||||
using var memoryStream = new MemoryStream((int)entry.Length);
|
||||
|
||||
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);
|
||||
zipStream.CopyTo(memoryStream); // can't use CopyToAsync with ZipArchive
|
||||
}
|
||||
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
Image.Source = await ImageLoader.LoadImageAsync(memoryStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(GroundOverlay));
|
||||
|
||||
public static readonly DependencyProperty SourcePathProperty =
|
||||
DependencyPropertyHelper.Register<GroundOverlay, string>(nameof(SourcePath), null,
|
||||
async (groundOverlay, oldValue, newValue) => await groundOverlay.LoadAsync(newValue));
|
||||
|
||||
public string SourcePath
|
||||
{
|
||||
get => (string)GetValue(SourcePathProperty);
|
||||
set => SetValue(SourcePathProperty, value);
|
||||
}
|
||||
|
||||
public static async Task<GroundOverlay> CreateAsync(string sourcePath)
|
||||
{
|
||||
var groundOverlay = new GroundOverlay();
|
||||
|
||||
await groundOverlay.LoadAsync(sourcePath);
|
||||
|
||||
return groundOverlay;
|
||||
}
|
||||
|
||||
public async Task LoadAsync(string sourcePath)
|
||||
{
|
||||
List<ImageOverlay> imageOverlays = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(sourcePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var ext = Path.GetExtension(sourcePath).ToLower();
|
||||
|
||||
if (ext == ".kmz")
|
||||
{
|
||||
imageOverlays = await LoadImageOverlaysFromArchive(sourcePath);
|
||||
}
|
||||
else if (ext == ".kml")
|
||||
{
|
||||
imageOverlays = await LoadImageOverlaysFromFile(sourcePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "Failed loading from {path}", sourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
Children.Clear();
|
||||
|
||||
if (imageOverlays != null)
|
||||
{
|
||||
foreach (var imageOverlay in imageOverlays)
|
||||
{
|
||||
Children.Add(imageOverlay.Image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<ImageOverlay>> LoadImageOverlaysFromArchive(string archiveFilePath)
|
||||
{
|
||||
using var archive = ZipFile.OpenRead(archiveFilePath);
|
||||
|
||||
var docEntry = archive.GetEntry("doc.kml") ??
|
||||
archive.Entries.FirstOrDefault(e => e.Name.EndsWith(".kml")) ??
|
||||
throw new ArgumentException($"No KML entry found in {archiveFilePath}.");
|
||||
XElement element;
|
||||
|
||||
using (var stream = docEntry.Open())
|
||||
{
|
||||
element = await XDocument.LoadRootElementAsync(stream);
|
||||
}
|
||||
|
||||
return await LoadImageOverlays(element, imageOverlay => imageOverlay.LoadImage(archive));
|
||||
}
|
||||
|
||||
private static async Task<List<ImageOverlay>> LoadImageOverlaysFromFile(string docFilePath)
|
||||
{
|
||||
var docUri = new Uri(FilePath.GetFullPath(docFilePath));
|
||||
XElement element;
|
||||
|
||||
using (var stream = File.OpenRead(docUri.AbsolutePath))
|
||||
{
|
||||
element = await XDocument.LoadRootElementAsync(stream);
|
||||
}
|
||||
|
||||
return await LoadImageOverlays(element, imageOverlay => imageOverlay.LoadImage(docUri));
|
||||
}
|
||||
|
||||
private static async Task<List<ImageOverlay>> LoadImageOverlays(XElement rootElement, Func<ImageOverlay, Task> loadFunc)
|
||||
{
|
||||
var imageOverlays = ReadImageOverlays(rootElement);
|
||||
|
||||
await Task.WhenAll(imageOverlays.Select(loadFunc));
|
||||
|
||||
return imageOverlays;
|
||||
}
|
||||
|
||||
private static List<ImageOverlay> ReadImageOverlays(XElement rootElement)
|
||||
{
|
||||
var ns = rootElement.Name.Namespace;
|
||||
var docElement = rootElement.Element(ns + "Document") ?? rootElement;
|
||||
var imageOverlays = new List<ImageOverlay>();
|
||||
|
||||
foreach (var folderElement in docElement.Elements(ns + "Folder"))
|
||||
{
|
||||
foreach (var groundOverlayElement in folderElement.Elements(ns + "GroundOverlay"))
|
||||
{
|
||||
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 drawOrder = groundOverlayElement.Element(ns + "drawOrder")?.Value;
|
||||
var zIndex = drawOrder != null ? int.Parse(drawOrder) : 0;
|
||||
|
||||
if (latLonBox != null && path != null)
|
||||
{
|
||||
imageOverlays.Add(new ImageOverlay(path, latLonBox, zIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imageOverlays;
|
||||
}
|
||||
|
||||
private static BoundingBox ReadLatLonBox(XElement latLonBoxElement)
|
||||
{
|
||||
var ns = latLonBoxElement.Name.Namespace;
|
||||
var north = double.NaN;
|
||||
var south = double.NaN;
|
||||
var east = double.NaN;
|
||||
var west = double.NaN;
|
||||
|
||||
var value = latLonBoxElement.Element(ns + "north")?.Value;
|
||||
if (value != null)
|
||||
{
|
||||
north = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
value = latLonBoxElement.Element(ns + "south")?.Value;
|
||||
if (value != null)
|
||||
{
|
||||
south = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
value = latLonBoxElement.Element(ns + "east")?.Value;
|
||||
if (value != null)
|
||||
{
|
||||
east = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
value = latLonBoxElement.Element(ns + "west")?.Value;
|
||||
if (value != null)
|
||||
{
|
||||
west = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (double.IsNaN(north) || double.IsNaN(south) ||
|
||||
double.IsNaN(east) || double.IsNaN(west) ||
|
||||
north <= south || east <= west)
|
||||
{
|
||||
throw new FormatException("Invalid LatLonBox");
|
||||
}
|
||||
|
||||
return new BoundingBox(south, west, north, east);
|
||||
}
|
||||
}
|
||||
|
||||
private static ILogger Logger => field ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(GroundOverlay));
|
||||
|
||||
public static readonly DependencyProperty SourcePathProperty =
|
||||
DependencyPropertyHelper.Register<GroundOverlay, string>(nameof(SourcePath), null,
|
||||
async (groundOverlay, oldValue, newValue) => await groundOverlay.LoadAsync(newValue));
|
||||
|
||||
public string SourcePath
|
||||
{
|
||||
get => (string)GetValue(SourcePathProperty);
|
||||
set => SetValue(SourcePathProperty, value);
|
||||
}
|
||||
|
||||
public static async Task<GroundOverlay> CreateAsync(string sourcePath)
|
||||
{
|
||||
var groundOverlay = new GroundOverlay();
|
||||
|
||||
await groundOverlay.LoadAsync(sourcePath);
|
||||
|
||||
return groundOverlay;
|
||||
}
|
||||
|
||||
public async Task LoadAsync(string sourcePath)
|
||||
{
|
||||
List<ImageOverlay> imageOverlays = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(sourcePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var ext = Path.GetExtension(sourcePath).ToLower();
|
||||
|
||||
if (ext == ".kmz")
|
||||
{
|
||||
imageOverlays = await LoadImageOverlaysFromArchive(sourcePath);
|
||||
}
|
||||
else if (ext == ".kml")
|
||||
{
|
||||
imageOverlays = await LoadImageOverlaysFromFile(sourcePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "Failed loading from {path}", sourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
Children.Clear();
|
||||
|
||||
if (imageOverlays != null)
|
||||
{
|
||||
foreach (var imageOverlay in imageOverlays)
|
||||
{
|
||||
Children.Add(imageOverlay.Image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<ImageOverlay>> LoadImageOverlaysFromArchive(string archiveFilePath)
|
||||
{
|
||||
using var archive = ZipFile.OpenRead(archiveFilePath);
|
||||
|
||||
var docEntry = archive.GetEntry("doc.kml") ??
|
||||
archive.Entries.FirstOrDefault(e => e.Name.EndsWith(".kml")) ??
|
||||
throw new ArgumentException($"No KML entry found in {archiveFilePath}.");
|
||||
XElement element;
|
||||
|
||||
using (var stream = docEntry.Open())
|
||||
{
|
||||
element = await XDocument.LoadRootElementAsync(stream);
|
||||
}
|
||||
|
||||
return await LoadImageOverlays(element, imageOverlay => imageOverlay.LoadImage(archive));
|
||||
}
|
||||
|
||||
private static async Task<List<ImageOverlay>> LoadImageOverlaysFromFile(string docFilePath)
|
||||
{
|
||||
var docUri = new Uri(FilePath.GetFullPath(docFilePath));
|
||||
XElement element;
|
||||
|
||||
using (var stream = File.OpenRead(docUri.AbsolutePath))
|
||||
{
|
||||
element = await XDocument.LoadRootElementAsync(stream);
|
||||
}
|
||||
|
||||
return await LoadImageOverlays(element, imageOverlay => imageOverlay.LoadImage(docUri));
|
||||
}
|
||||
|
||||
private static async Task<List<ImageOverlay>> LoadImageOverlays(XElement rootElement, Func<ImageOverlay, Task> loadFunc)
|
||||
{
|
||||
var imageOverlays = ReadImageOverlays(rootElement);
|
||||
|
||||
await Task.WhenAll(imageOverlays.Select(loadFunc));
|
||||
|
||||
return imageOverlays;
|
||||
}
|
||||
|
||||
private static List<ImageOverlay> ReadImageOverlays(XElement rootElement)
|
||||
{
|
||||
var ns = rootElement.Name.Namespace;
|
||||
var docElement = rootElement.Element(ns + "Document") ?? rootElement;
|
||||
var imageOverlays = new List<ImageOverlay>();
|
||||
|
||||
foreach (var folderElement in docElement.Elements(ns + "Folder"))
|
||||
{
|
||||
foreach (var groundOverlayElement in folderElement.Elements(ns + "GroundOverlay"))
|
||||
{
|
||||
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 drawOrder = groundOverlayElement.Element(ns + "drawOrder")?.Value;
|
||||
var zIndex = drawOrder != null ? int.Parse(drawOrder) : 0;
|
||||
|
||||
if (latLonBox != null && path != null)
|
||||
{
|
||||
imageOverlays.Add(new ImageOverlay(path, latLonBox, zIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imageOverlays;
|
||||
}
|
||||
|
||||
private static BoundingBox ReadLatLonBox(XElement latLonBoxElement)
|
||||
{
|
||||
var ns = latLonBoxElement.Name.Namespace;
|
||||
var north = double.NaN;
|
||||
var south = double.NaN;
|
||||
var east = double.NaN;
|
||||
var west = double.NaN;
|
||||
|
||||
var value = latLonBoxElement.Element(ns + "north")?.Value;
|
||||
if (value != null)
|
||||
{
|
||||
north = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
value = latLonBoxElement.Element(ns + "south")?.Value;
|
||||
if (value != null)
|
||||
{
|
||||
south = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
value = latLonBoxElement.Element(ns + "east")?.Value;
|
||||
if (value != null)
|
||||
{
|
||||
east = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
value = latLonBoxElement.Element(ns + "west")?.Value;
|
||||
if (value != null)
|
||||
{
|
||||
west = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (double.IsNaN(north) || double.IsNaN(south) ||
|
||||
double.IsNaN(east) || double.IsNaN(west) ||
|
||||
north <= south || east <= west)
|
||||
{
|
||||
throw new FormatException("Invalid LatLonBox");
|
||||
}
|
||||
|
||||
return new BoundingBox(south, west, north, east);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,356 +8,355 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
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>
|
||||
/// 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
|
||||
public ImageFileCache(IOptions<ImageFileCacheOptions> optionsAccessor, ILoggerFactory loggerFactory = null)
|
||||
: this(optionsAccessor.Value, loggerFactory)
|
||||
{
|
||||
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 ImageFileCache(ImageFileCacheOptions options, ILoggerFactory loggerFactory = 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);
|
||||
}
|
||||
|
||||
public ImageFileCache(IOptions<ImageFileCacheOptions> optionsAccessor, ILoggerFactory loggerFactory = null)
|
||||
: this(optionsAccessor.Value, loggerFactory)
|
||||
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);
|
||||
|
||||
public ImageFileCache(ImageFileCacheOptions options, ILoggerFactory loggerFactory = 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)
|
||||
if (value == null)
|
||||
{
|
||||
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);
|
||||
|
||||
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())
|
||||
{
|
||||
stream.Write(value, 0, value.Length);
|
||||
}
|
||||
var options = new DistributedCacheEntryOptions { AbsoluteExpiration = file.CreationTime };
|
||||
|
||||
SetExpiration(file, options);
|
||||
memoryCache.Set(key, value, options);
|
||||
|
||||
logger?.LogDebug("Wrote {name}", file.FullName);
|
||||
logger?.LogDebug("Read {name}", file.FullName);
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(key) && value != null && options != null)
|
||||
{
|
||||
await memoryCache.SetAsync(key, value, options, token).ConfigureAwait(false);
|
||||
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 && 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())
|
||||
{
|
||||
await stream.WriteAsync(value, 0, value.Length, token).ConfigureAwait(false);
|
||||
}
|
||||
var options = new DistributedCacheEntryOptions { AbsoluteExpiration = file.CreationTime };
|
||||
|
||||
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)
|
||||
{
|
||||
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.Refresh(key);
|
||||
}
|
||||
}
|
||||
memoryCache.Set(key, value, options);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
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;
|
||||
var file = GetFile(key);
|
||||
|
||||
try
|
||||
{
|
||||
deletedFileCount = directory.EnumerateDirectories().Sum(ScanDirectory);
|
||||
if (file != null && value?.Length > 0)
|
||||
{
|
||||
file.Directory.Create();
|
||||
|
||||
foreach (var file in directory.EnumerateFiles()
|
||||
.Where(file => file.CreationTime > file.LastWriteTime &&
|
||||
file.CreationTime <= DateTime.Now))
|
||||
using (var stream = file.Create())
|
||||
{
|
||||
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();
|
||||
deletedFileCount++;
|
||||
}
|
||||
|
||||
if (!directory.EnumerateFileSystemInfos().Any())
|
||||
{
|
||||
directory.Delete();
|
||||
}
|
||||
}
|
||||
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
|
||||
{
|
||||
file = new FileInfo(Path.Combine(rootDirectory.FullName, Path.Combine(key.Split('/'))));
|
||||
if (file != null && file.Exists && !token.IsCancellationRequested)
|
||||
{
|
||||
file.Delete();
|
||||
}
|
||||
}
|
||||
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;
|
||||
#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>
|
||||
/// 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
|
||||
public static async Task<ImageSource> LoadImageAsync(byte[] buffer)
|
||||
{
|
||||
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
|
||||
{
|
||||
get => field ??= new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
||||
set;
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
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)
|
||||
if (offset < length) // 1.0 reported by caller
|
||||
{
|
||||
offset += read;
|
||||
|
||||
if (offset < length) // 1.0 reported by caller
|
||||
{
|
||||
progress.Report((double)offset / length);
|
||||
}
|
||||
progress.Report((double)offset / length);
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,46 @@
|
|||
using System.Collections.Generic;
|
||||
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)
|
||||
: base(tileMatrix.Width * tileMatrix.Height)
|
||||
{
|
||||
FillMatrix(source, tileMatrix.ZoomLevel, tileMatrix.XMin, tileMatrix.YMin, tileMatrix.XMax, tileMatrix.YMax, columnCount);
|
||||
}
|
||||
public ImageTileList(IEnumerable<ImageTile> source, TileMatrix tileMatrix, int columnCount)
|
||||
: base(tileMatrix.Width * tileMatrix.Height)
|
||||
{
|
||||
FillMatrix(source, tileMatrix.ZoomLevel, tileMatrix.XMin, tileMatrix.YMin, tileMatrix.XMax, tileMatrix.YMax, columnCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds existing ImageTile from the source collection or newly created ImageTile to fill the specified tile matrix.
|
||||
/// </summary>
|
||||
public void FillMatrix(IEnumerable<ImageTile> source, int zoomLevel, int xMin, int yMin, int xMax, int yMax, int columnCount)
|
||||
/// <summary>
|
||||
/// Adds existing ImageTile from the source collection or newly created ImageTile to fill the specified tile matrix.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
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
|
||||
}
|
||||
tile.IsPending = false;
|
||||
tile.Image.Source = equivalentTile.Image.Source; // no Opacity animation
|
||||
}
|
||||
|
||||
Add(tile);
|
||||
}
|
||||
|
||||
Add(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,129 +1,128 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MapControl
|
||||
{
|
||||
/// <summary>
|
||||
/// A geographic location with latitude and longitude values in degrees.
|
||||
/// </summary>
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// A geographic location with latitude and longitude values in degrees.
|
||||
/// </summary>
|
||||
#if UWP || WINUI
|
||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||
#else
|
||||
[System.ComponentModel.TypeConverter(typeof(LocationConverter))]
|
||||
[System.ComponentModel.TypeConverter(typeof(LocationConverter))]
|
||||
#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);
|
||||
public double Longitude => longitude;
|
||||
string[] values = null;
|
||||
|
||||
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)
|
||||
if (!string.IsNullOrEmpty(location))
|
||||
{
|
||||
string[] values = null;
|
||||
|
||||
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));
|
||||
values = location.Split(',');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a longitude to a value in the interval [-180 .. 180).
|
||||
/// </summary>
|
||||
public static double NormalizeLongitude(double longitude)
|
||||
if (values?.Length != 2)
|
||||
{
|
||||
var x = (longitude + 180d) % 360d;
|
||||
|
||||
return x < 0d ? x + 180d : x - 180d;
|
||||
throw new FormatException($"{nameof(Location)} string must contain a comma-separated pair of floating point numbers.");
|
||||
}
|
||||
|
||||
// Arithmetic mean radius (2*a + b) / 3 == (1 - f/3) * a.
|
||||
// See https://en.wikipedia.org/wiki/Earth_radius#Arithmetic_mean_radius.
|
||||
//
|
||||
public const double Wgs84MeanRadius = (1d - MapProjection.Wgs84Flattening / 3d) * MapProjection.Wgs84EquatorialRadius;
|
||||
return new Location(
|
||||
double.Parse(values[0], NumberStyles.Float, CultureInfo.InvariantCulture),
|
||||
double.Parse(values[1], NumberStyles.Float, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Normalizes a longitude to a value in the interval [-180 .. 180).
|
||||
/// </summary>
|
||||
public static double NormalizeLongitude(double longitude)
|
||||
{
|
||||
var x = (longitude + 180d) % 360d;
|
||||
|
||||
return (azimuth * 180d / Math.PI, distance * earthRadius);
|
||||
}
|
||||
return x < 0d ? x + 180d : x - 180d;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
// Arithmetic mean radius (2*a + b) / 3 == (1 - f/3) * a.
|
||||
// See https://en.wikipedia.org/wiki/Earth_radius#Arithmetic_mean_radius.
|
||||
//
|
||||
public const double Wgs84MeanRadius = (1d - MapProjection.Wgs84Flattening / 3d) * MapProjection.Wgs84EquatorialRadius;
|
||||
|
||||
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>
|
||||
/// 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 (azimuth * 180d / Math.PI, distance * earthRadius);
|
||||
}
|
||||
|
||||
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.Linq;
|
||||
|
||||
namespace MapControl
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of Locations with support for string parsing
|
||||
/// and calculation of great circle and rhumb line locations.
|
||||
/// </summary>
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// A collection of Locations with support for string parsing
|
||||
/// and calculation of great circle and rhumb line locations.
|
||||
/// </summary>
|
||||
#if UWP || WINUI
|
||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||
#else
|
||||
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
||||
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
||||
#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)
|
||||
: base(locations)
|
||||
Add(new Location(latitude, longitude));
|
||||
}
|
||||
|
||||
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)
|
||||
: base(locations)
|
||||
var lat1 = location1.Latitude * Math.PI / 180d;
|
||||
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)
|
||||
{
|
||||
var deltaLon = longitude - this[Count - 1].Longitude;
|
||||
|
||||
if (deltaLon < -180d)
|
||||
{
|
||||
longitude += 360d;
|
||||
}
|
||||
else if (deltaLon > 180)
|
||||
{
|
||||
longitude -= 360;
|
||||
}
|
||||
}
|
||||
|
||||
Add(new Location(latitude, longitude));
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(resolution), $"The {nameof(resolution)} argument must be greater than zero.");
|
||||
}
|
||||
|
||||
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>
|
||||
/// 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)
|
||||
if (double.IsInfinity(y2))
|
||||
{
|
||||
return string.IsNullOrEmpty(locations)
|
||||
? new LocationCollection()
|
||||
: new LocationCollection(locations
|
||||
.Split([' ', ';'], StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(Location.Parse));
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(location2), $"The {nameof(location2)} argument must have an absolute latitude value of less than 90.");
|
||||
}
|
||||
|
||||
/// <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)
|
||||
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)
|
||||
{
|
||||
if (resolution <= 0d)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(resolution), $"The {nameof(resolution)} argument must be greater than zero.");
|
||||
}
|
||||
var lat = (lat1 + lat2) * Math.PI / 360d; // mean latitude
|
||||
|
||||
var lat1 = location1.Latitude * Math.PI / 180d;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
locations.Add(location2.Latitude, location2.Longitude);
|
||||
|
||||
return locations;
|
||||
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
|
||||
}
|
||||
|
||||
/// <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)
|
||||
var n = (int)Math.Ceiling(s12 / resolution);
|
||||
|
||||
var locations = new LocationCollection(new Location(lat1, lon1));
|
||||
|
||||
if (sec > secLimit)
|
||||
{
|
||||
if (resolution <= 0d)
|
||||
for (var i = 1; i < n; i++)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(resolution), $"The {nameof(resolution)} argument must be greater than zero.");
|
||||
var lon = lon1 + i * dlon / n;
|
||||
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;
|
||||
#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>
|
||||
/// 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>
|
||||
public partial class Map : MapBase
|
||||
public double MouseWheelZoomDelta
|
||||
{
|
||||
public static readonly DependencyProperty MouseWheelZoomDeltaProperty =
|
||||
DependencyPropertyHelper.Register<Map, double>(nameof(MouseWheelZoomDelta), 0.25);
|
||||
get => (double)GetValue(MouseWheelZoomDeltaProperty);
|
||||
set => SetValue(MouseWheelZoomDeltaProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty MouseWheelZoomAnimatedProperty =
|
||||
DependencyPropertyHelper.Register<Map, bool>(nameof(MouseWheelZoomAnimated), true);
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// Gets or sets the amount by which the ZoomLevel property changes by a MouseWheel event.
|
||||
/// The default value is 0.25.
|
||||
/// </summary>
|
||||
public double MouseWheelZoomDelta
|
||||
private void OnMouseWheel(Point position, double delta)
|
||||
{
|
||||
var zoomLevel = TargetZoomLevel + MouseWheelZoomDelta * delta;
|
||||
var animated = false;
|
||||
|
||||
if (delta <= -1d || delta >= 1d)
|
||||
{
|
||||
get => (double)GetValue(MouseWheelZoomDeltaProperty);
|
||||
set => SetValue(MouseWheelZoomDeltaProperty, value);
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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);
|
||||
}
|
||||
ZoomMap(position, zoomLevel, animated);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,240 +17,239 @@ using Avalonia.Controls;
|
|||
using Brush = Avalonia.Media.IBrush;
|
||||
#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; }
|
||||
Brush MapForeground { get; }
|
||||
get => GetValue(MapLayerProperty);
|
||||
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 =
|
||||
DependencyPropertyHelper.Register<MapBase, object>(nameof(MapLayer), null,
|
||||
(map, oldValue, newValue) => map.MapLayerPropertyChanged(oldValue, newValue));
|
||||
get => (IEnumerable)GetValue(MapLayersSourceProperty);
|
||||
set => SetValue(MapLayersSourceProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty MapLayersSourceProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, IEnumerable>(nameof(MapLayersSource), null,
|
||||
(map, oldValue, newValue) => map.MapLayersSourcePropertyChanged(oldValue, newValue));
|
||||
private void MapLayerPropertyChanged(object oldLayer, object newLayer)
|
||||
{
|
||||
bool IsMapLayer(object layer) => Children.Count > 0 &&
|
||||
(Children[0] == layer as FrameworkElement ||
|
||||
((FrameworkElement)Children[0]).DataContext == layer);
|
||||
|
||||
/// <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
|
||||
if (oldLayer != null && IsMapLayer(oldLayer))
|
||||
{
|
||||
get => GetValue(MapLayerProperty);
|
||||
set => SetValue(MapLayerProperty, value);
|
||||
RemoveChildElement(0);
|
||||
}
|
||||
|
||||
/// <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
|
||||
if (newLayer != null && !IsMapLayer(newLayer))
|
||||
{
|
||||
get => (IEnumerable)GetValue(MapLayersSourceProperty);
|
||||
set => SetValue(MapLayersSourceProperty, value);
|
||||
InsertChildElement(0, GetMapLayer(newLayer));
|
||||
}
|
||||
}
|
||||
|
||||
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 &&
|
||||
(Children[0] == layer as FrameworkElement ||
|
||||
((FrameworkElement)Children[0]).DataContext == layer);
|
||||
|
||||
if (oldLayer != null && IsMapLayer(oldLayer))
|
||||
if (newLayers is INotifyCollectionChanged incc)
|
||||
{
|
||||
RemoveChildElement(0);
|
||||
incc.CollectionChanged += MapLayersSourceCollectionChanged;
|
||||
}
|
||||
|
||||
if (newLayer != null && !IsMapLayer(newLayer))
|
||||
{
|
||||
InsertChildElement(0, GetMapLayer(newLayer));
|
||||
}
|
||||
AddMapLayers(newLayers, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void MapLayersSourcePropertyChanged(IEnumerable oldLayers, IEnumerable newLayers)
|
||||
private void MapLayersSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
if (oldLayers != null)
|
||||
{
|
||||
if (oldLayers is INotifyCollectionChanged incc)
|
||||
{
|
||||
incc.CollectionChanged -= MapLayersSourceCollectionChanged;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
AddMapLayers(e.NewItems, e.NewStartingIndex);
|
||||
break;
|
||||
|
||||
RemoveMapLayers(oldLayers, 0);
|
||||
}
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
RemoveMapLayers(e.OldItems, e.OldStartingIndex);
|
||||
break;
|
||||
|
||||
if (newLayers != null)
|
||||
{
|
||||
if (newLayers is INotifyCollectionChanged incc)
|
||||
{
|
||||
incc.CollectionChanged += MapLayersSourceCollectionChanged;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
RemoveMapLayers(e.OldItems, e.OldStartingIndex);
|
||||
AddMapLayers(e.NewItems, e.NewStartingIndex);
|
||||
break;
|
||||
|
||||
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.
|
||||
//
|
||||
Dispatcher.Invoke(() => AddMapLayers(mapLayers, index), DispatcherPriority.DataBind);
|
||||
//
|
||||
Dispatcher.Invoke(() => AddMapLayers(mapLayers, index), DispatcherPriority.DataBind);
|
||||
#else
|
||||
AddMapLayers(mapLayers, index);
|
||||
AddMapLayers(mapLayers, index);
|
||||
#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)
|
||||
{
|
||||
foreach (var mapLayer in mapLayers)
|
||||
{
|
||||
InsertChildElement(index, mapLayer);
|
||||
Children.Insert(index, element);
|
||||
}
|
||||
|
||||
if (index++ == 0)
|
||||
{
|
||||
MapLayer = mapLayer;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveMapLayers(IEnumerable layers, int index)
|
||||
{
|
||||
foreach (var _ in layers)
|
||||
{
|
||||
RemoveChildElement(index);
|
||||
}
|
||||
Children.RemoveAt(index);
|
||||
}
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
MapLayer = null;
|
||||
}
|
||||
private FrameworkElement GetMapLayer(object layer)
|
||||
{
|
||||
FrameworkElement mapLayer = null;
|
||||
|
||||
if (layer != null)
|
||||
{
|
||||
mapLayer = layer as FrameworkElement ?? TryLoadDataTemplate(layer);
|
||||
}
|
||||
|
||||
private void InsertChildElement(int index, FrameworkElement element)
|
||||
{
|
||||
if (index == 0 && element is IMapLayer mapLayer)
|
||||
{
|
||||
if (mapLayer.MapBackground != null)
|
||||
{
|
||||
Background = mapLayer.MapBackground;
|
||||
}
|
||||
return mapLayer ?? new MapTileLayer();
|
||||
}
|
||||
|
||||
if (mapLayer.MapForeground != null)
|
||||
{
|
||||
Foreground = mapLayer.MapForeground;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
private FrameworkElement TryLoadDataTemplate(object layer)
|
||||
{
|
||||
FrameworkElement element = null;
|
||||
#if AVALONIA
|
||||
if (this.TryFindResource(layer.GetType().FullName, out object value) &&
|
||||
value is Avalonia.Markup.Xaml.Templates.DataTemplate template)
|
||||
{
|
||||
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;
|
||||
if (this.TryFindResource(layer.GetType().FullName, out object value) &&
|
||||
value is Avalonia.Markup.Xaml.Templates.DataTemplate template)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
#if UWP || WINUI
|
||||
private static object TryFindResource(FrameworkElement element, object key)
|
||||
{
|
||||
return element.Resources.ContainsKey(key)
|
||||
? element.Resources[key]
|
||||
: element.Parent is FrameworkElement parent
|
||||
? TryFindResource(parent, key)
|
||||
: null;
|
||||
}
|
||||
#endif
|
||||
private static object TryFindResource(FrameworkElement element, object key)
|
||||
{
|
||||
return element.Resources.ContainsKey(key)
|
||||
? element.Resources[key]
|
||||
: element.Parent is FrameworkElement parent
|
||||
? TryFindResource(parent, key)
|
||||
: null;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,431 +13,430 @@ using Avalonia;
|
|||
using Brush = Avalonia.Media.IBrush;
|
||||
#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>
|
||||
/// 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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
else
|
||||
{
|
||||
get => (Brush)GetValue(ForegroundProperty);
|
||||
set => SetValue(ForegroundProperty, value);
|
||||
}
|
||||
SetTransformCenter(center);
|
||||
viewCenter = new Point(viewCenter.X + translation.X, viewCenter.Y + translation.Y);
|
||||
|
||||
/// <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)
|
||||
if (rotation != 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>
|
||||
/// 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)
|
||||
if (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);
|
||||
viewCenter = new Point(viewCenter.X + translation.X, viewCenter.Y + translation.Y);
|
||||
|
||||
if (rotation != 0d)
|
||||
{
|
||||
var heading = CoerceHeadingProperty(Heading - rotation);
|
||||
SetValueInternal(HeadingProperty, heading);
|
||||
SetValueInternal(TargetHeadingProperty, heading);
|
||||
}
|
||||
|
||||
if (scale != 1d)
|
||||
{
|
||||
var zoomLevel = CoerceZoomLevelProperty(ZoomLevel + Math.Log(scale, 2d));
|
||||
SetValueInternal(ZoomLevelProperty, zoomLevel);
|
||||
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
||||
}
|
||||
|
||||
TargetZoomLevel = zoomLevel;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ZoomLevel != zoomLevel)
|
||||
{
|
||||
SetTransformCenter(center);
|
||||
SetValueInternal(ZoomLevelProperty, zoomLevel);
|
||||
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
||||
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)
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
SetTransformCenter(center);
|
||||
TargetZoomLevel = zoomLevel;
|
||||
}
|
||||
latitude = Math.Min(Math.Max(latitude, -maxLatitude), maxLatitude);
|
||||
resetTransformCenter = true;
|
||||
}
|
||||
else
|
||||
|
||||
if (!center.Equals(latitude, longitude))
|
||||
{
|
||||
if (ZoomLevel != zoomLevel)
|
||||
{
|
||||
SetTransformCenter(center);
|
||||
SetValueInternal(ZoomLevelProperty, zoomLevel);
|
||||
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
||||
UpdateTransform(true);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
ViewScale = ViewTransform.Scale;
|
||||
|
||||
internal bool InsideViewBounds(Point point)
|
||||
{
|
||||
return point.X >= 0d && point.Y >= 0d && point.X <= ActualWidth && point.Y <= ActualHeight;
|
||||
}
|
||||
// Check if view center has moved across 180° longitude.
|
||||
//
|
||||
transformCenterChanged = transformCenterChanged || Math.Abs(Center.Longitude - centerLongitude) > 180d;
|
||||
centerLongitude = Center.Longitude;
|
||||
|
||||
internal double NearestLongitude(double longitude)
|
||||
{
|
||||
longitude = Location.NormalizeLongitude(longitude);
|
||||
|
||||
var offset = longitude - Center.Longitude;
|
||||
OnViewportChanged(new ViewportChangedEventArgs(projectionChanged, transformCenterChanged));
|
||||
}
|
||||
|
||||
if (offset > 180d)
|
||||
{
|
||||
longitude = Center.Longitude + (offset % 360d) - 360d;
|
||||
}
|
||||
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);
|
||||
}
|
||||
protected override void OnViewportChanged(ViewportChangedEventArgs e)
|
||||
{
|
||||
base.OnViewportChanged(e);
|
||||
ViewportChanged?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,84 +8,83 @@ using Microsoft.UI.Xaml;
|
|||
using Avalonia;
|
||||
#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>
|
||||
/// 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
|
||||
public static readonly DependencyProperty BorderWidthProperty =
|
||||
DependencyPropertyHelper.Register<MapBorderPanel, double>(nameof(BorderWidth));
|
||||
|
||||
public static readonly DependencyProperty OnBorderProperty =
|
||||
DependencyPropertyHelper.RegisterAttached<bool>("OnBorder", typeof(MapBorderPanel));
|
||||
|
||||
public double BorderWidth
|
||||
{
|
||||
public static readonly DependencyProperty BorderWidthProperty =
|
||||
DependencyPropertyHelper.Register<MapBorderPanel, double>(nameof(BorderWidth));
|
||||
get => (double)GetValue(BorderWidthProperty);
|
||||
set => SetValue(BorderWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty OnBorderProperty =
|
||||
DependencyPropertyHelper.RegisterAttached<bool>("OnBorder", typeof(MapBorderPanel));
|
||||
public static bool GetOnBorder(FrameworkElement element)
|
||||
{
|
||||
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);
|
||||
set => SetValue(BorderWidthProperty, value);
|
||||
}
|
||||
var dx = position.X - w / 2d;
|
||||
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)
|
||||
{
|
||||
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)
|
||||
if (dx < 0d)
|
||||
{
|
||||
var dx = position.X - w / 2d;
|
||||
var dy = position.Y - h / 2d;
|
||||
var cx = (maxX - minX) / 2d;
|
||||
var cy = (maxY - minY) / 2d;
|
||||
double x, y;
|
||||
x = minX;
|
||||
y = minY + cy - cx * dy / dx;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = maxX;
|
||||
y = minY + cy + cx * dy / dx;
|
||||
}
|
||||
|
||||
if (dx < 0d)
|
||||
if (y < minY || y > maxY)
|
||||
{
|
||||
if (dy < 0d)
|
||||
{
|
||||
x = minX;
|
||||
y = minY + cy - cx * dy / dx;
|
||||
x = minX + cx - cy * dx / dy;
|
||||
y = minY;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = maxX;
|
||||
y = minY + cy + cx * dy / dx;
|
||||
x = minX + cx + cy * dx / dy;
|
||||
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);
|
||||
|
||||
return base.SetViewPosition(element, position);
|
||||
position = new Point(x, y);
|
||||
onBorder = true;
|
||||
}
|
||||
|
||||
element.SetValue(OnBorderProperty, onBorder);
|
||||
|
||||
return base.SetViewPosition(element, position);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,44 +11,43 @@ using Microsoft.UI.Xaml.Controls;
|
|||
using Avalonia.Controls;
|
||||
#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>
|
||||
/// ContentControl placed on a MapPanel at a geographic location specified by the Location property.
|
||||
/// Gets/sets MapPanel.Location.
|
||||
/// </summary>
|
||||
public partial class MapContentControl : ContentControl
|
||||
public Location Location
|
||||
{
|
||||
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>
|
||||
/// 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);
|
||||
}
|
||||
get => (Location)GetValue(LocationProperty);
|
||||
set => SetValue(LocationProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MapContentControl with a Pushpin Style.
|
||||
/// Gets/sets MapPanel.AutoCollapse.
|
||||
/// </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;
|
||||
#endif
|
||||
|
||||
namespace MapControl
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// Draws a map graticule, i.e. a lat/lon grid overlay.
|
||||
/// </summary>
|
||||
public partial class MapGraticule : MapGrid
|
||||
{
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
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
|
||||
{
|
||||
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 = [
|
||||
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 void DrawGraticule(PathFigureCollection figures, List<Label> labels)
|
||||
{
|
||||
var lineDistance = GetLineDistance(true);
|
||||
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}\"" :
|
||||
lineDistance < 1d ? "{0} {1}°{2:00}'" : "{0} {1}°";
|
||||
minLon = lon;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
maxLon = lon;
|
||||
}
|
||||
|
||||
private void DrawNormalGraticule(PathFigureCollection figures, List<Label> labels)
|
||||
if (minLon + 360d > maxLon)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
minLon -= lineDistance;
|
||||
}
|
||||
|
||||
private void DrawGraticule(PathFigureCollection figures, List<Label> labels)
|
||||
if (maxLon - 360d < minLon)
|
||||
{
|
||||
var lineDistance = GetLineDistance(true);
|
||||
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));
|
||||
}
|
||||
}
|
||||
maxLon += lineDistance;
|
||||
}
|
||||
|
||||
private bool DrawMeridian(PathFigureCollection figures, double lon,
|
||||
double latStep, ref double minLat, ref double maxLat)
|
||||
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>();
|
||||
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);
|
||||
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)
|
||||
{
|
||||
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)
|
||||
if (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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
#endif
|
||||
|
||||
namespace MapControl
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// Base class of map grid or graticule overlays.
|
||||
/// </summary>
|
||||
public abstract partial class MapGrid
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class of map grid or graticule overlays.
|
||||
/// </summary>
|
||||
public abstract partial class MapGrid
|
||||
protected class Label(string text, double x, double y, double rotation,
|
||||
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment verticalAlignment = VerticalAlignment.Center)
|
||||
{
|
||||
protected class Label(string text, double x, double y, double rotation,
|
||||
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment verticalAlignment = VerticalAlignment.Center)
|
||||
public string Text => text;
|
||||
public double X => x;
|
||||
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;
|
||||
public double X => x;
|
||||
public double Y => y;
|
||||
public double Rotation => rotation;
|
||||
public HorizontalAlignment HorizontalAlignment => horizontalAlignment;
|
||||
public VerticalAlignment VerticalAlignment => verticalAlignment;
|
||||
}
|
||||
StartPoint = p1,
|
||||
IsClosed = false,
|
||||
IsFilled = false
|
||||
};
|
||||
|
||||
public static readonly DependencyProperty MinLineDistanceProperty =
|
||||
DependencyPropertyHelper.Register<MapGrid, double>(nameof(MinLineDistance), 150d);
|
||||
figure.Segments.Add(new LineSegment { Point = p2 });
|
||||
return figure;
|
||||
}
|
||||
|
||||
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
|
||||
protected static PathFigure CreatePolylineFigure(IEnumerable<Point> points)
|
||||
{
|
||||
var figure = new PathFigure
|
||||
{
|
||||
get => (double)GetValue(MinLineDistanceProperty);
|
||||
set => SetValue(MinLineDistanceProperty, value);
|
||||
}
|
||||
StartPoint = points.First(),
|
||||
IsClosed = false,
|
||||
IsFilled = false
|
||||
};
|
||||
|
||||
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
|
||||
{
|
||||
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;
|
||||
}
|
||||
figure.Segments.Add(CreatePolyLineSegment(points.Skip(1)));
|
||||
return figure;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,215 +21,214 @@ using Brush = Avalonia.Media.IBrush;
|
|||
using ImageSource = Avalonia.Media.IImage;
|
||||
#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>
|
||||
/// 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
|
||||
public static readonly DependencyProperty DescriptionProperty =
|
||||
DependencyPropertyHelper.Register<MapImageLayer, string>(nameof(Description));
|
||||
|
||||
public static readonly DependencyProperty RelativeImageSizeProperty =
|
||||
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 =
|
||||
DependencyPropertyHelper.Register<MapImageLayer, string>(nameof(Description));
|
||||
IsHitTestVisible = false;
|
||||
|
||||
public static readonly DependencyProperty RelativeImageSizeProperty =
|
||||
DependencyPropertyHelper.Register<MapImageLayer, double>(nameof(RelativeImageSize), 1d);
|
||||
loadingProgress = new Progress<double>(p => SetValue(LoadingProgressProperty, p));
|
||||
|
||||
public static readonly DependencyProperty UpdateIntervalProperty =
|
||||
DependencyPropertyHelper.Register<MapImageLayer, TimeSpan>(nameof(UpdateInterval), TimeSpan.FromSeconds(0.2),
|
||||
(layer, oldValue, newValue) => layer.updateTimer.Interval = newValue);
|
||||
updateTimer = new UpdateTimer { Interval = UpdateInterval };
|
||||
updateTimer.Tick += async (_, _) => await UpdateImageAsync();
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty UpdateWhileViewportChangingProperty =
|
||||
DependencyPropertyHelper.Register<MapImageLayer, bool>(nameof(UpdateWhileViewportChanging));
|
||||
/// <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);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty MapBackgroundProperty =
|
||||
DependencyPropertyHelper.Register<MapImageLayer, Brush>(nameof(MapBackground));
|
||||
/// <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);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty MapForegroundProperty =
|
||||
DependencyPropertyHelper.Register<MapImageLayer, Brush>(nameof(MapForeground));
|
||||
/// <summary>
|
||||
/// Minimum time interval between images updates.
|
||||
/// </summary>
|
||||
public TimeSpan UpdateInterval
|
||||
{
|
||||
get => (TimeSpan)GetValue(UpdateIntervalProperty);
|
||||
set => SetValue(UpdateIntervalProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty LoadingProgressProperty =
|
||||
DependencyPropertyHelper.Register<MapImageLayer, double>(nameof(LoadingProgress), 1d);
|
||||
/// <summary>
|
||||
/// 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;
|
||||
private readonly UpdateTimer updateTimer;
|
||||
private bool updateInProgress;
|
||||
/// <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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
topImage.Opacity = 1d;
|
||||
Children[0].Opacity = 0d;
|
||||
}
|
||||
topImage.Opacity = 1d;
|
||||
Children[0].Opacity = 0d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,101 +15,100 @@ using Avalonia.Controls;
|
|||
using Avalonia.Media;
|
||||
#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>
|
||||
/// Container class for an item in a MapItemsControl.
|
||||
/// Gets/sets MapPanel.Location.
|
||||
/// </summary>
|
||||
public partial class MapItem : ListBoxItem, IMapElement
|
||||
public Location Location
|
||||
{
|
||||
public static readonly DependencyProperty LocationProperty =
|
||||
DependencyPropertyHelper.AddOwner<MapItem, Location>(
|
||||
nameof(Location), MapPanel.LocationProperty, (item, _, _) => item.UpdateMapTransform());
|
||||
get => (Location)GetValue(LocationProperty);
|
||||
set => SetValue(LocationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty AutoCollapseProperty =
|
||||
DependencyPropertyHelper.AddOwner<MapItem, bool>(
|
||||
nameof(AutoCollapse), MapPanel.AutoCollapseProperty);
|
||||
/// <summary>
|
||||
/// Gets/sets MapPanel.AutoCollapse.
|
||||
/// </summary>
|
||||
public bool AutoCollapse
|
||||
{
|
||||
get => (bool)GetValue(AutoCollapseProperty);
|
||||
set => SetValue(AutoCollapseProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets/sets MapPanel.Location.
|
||||
/// </summary>
|
||||
public Location Location
|
||||
/// <summary>
|
||||
/// Implements IMapElement.ParentMap.
|
||||
/// </summary>
|
||||
public MapBase ParentMap
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
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>
|
||||
/// Implements IMapElement.ParentMap.
|
||||
/// </summary>
|
||||
public MapBase ParentMap
|
||||
{
|
||||
get;
|
||||
set
|
||||
if (field != null)
|
||||
{
|
||||
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.
|
||||
//
|
||||
field.ViewportChanged += OnViewportChanged;
|
||||
ParentMap.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)
|
||||
{
|
||||
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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,126 +18,125 @@ using Avalonia.Data;
|
|||
using PropertyPath = System.String;
|
||||
#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>
|
||||
/// 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>
|
||||
public partial class MapItemsControl : ListBox
|
||||
public string LocationMemberPath
|
||||
{
|
||||
public static readonly DependencyProperty LocationMemberPathProperty =
|
||||
DependencyPropertyHelper.Register<MapItemsControl, string>(nameof(LocationMemberPath));
|
||||
get => (string)GetValue(LocationMemberPathProperty);
|
||||
set => SetValue(LocationMemberPathProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path to a source property for binding the Location property of MapItem containers.
|
||||
/// </summary>
|
||||
public string LocationMemberPath
|
||||
public void SelectItems(Predicate<object> predicate)
|
||||
{
|
||||
if (SelectionMode == SelectionMode.Single)
|
||||
{
|
||||
get => (string)GetValue(LocationMemberPathProperty);
|
||||
set => SetValue(LocationMemberPathProperty, value);
|
||||
throw new InvalidOperationException("SelectionMode must not be Single");
|
||||
}
|
||||
|
||||
public void SelectItems(Predicate<object> predicate)
|
||||
foreach (var item in Items)
|
||||
{
|
||||
if (SelectionMode == SelectionMode.Single)
|
||||
{
|
||||
throw new InvalidOperationException("SelectionMode must not be Single");
|
||||
}
|
||||
var selected = predicate(item);
|
||||
|
||||
foreach (var item in Items)
|
||||
if (selected != SelectedItems.Contains(item))
|
||||
{
|
||||
var selected = predicate(item);
|
||||
|
||||
if (selected != SelectedItems.Contains(item))
|
||||
if (selected)
|
||||
{
|
||||
if (selected)
|
||||
{
|
||||
SelectedItems.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedItems.Remove(item);
|
||||
}
|
||||
SelectedItems.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
SelectedItems.Remove(item);
|
||||
}
|
||||
|
||||
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;
|
||||
#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>
|
||||
/// 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.
|
||||
/// Gets or sets the Locations that define the multi-polygon points.
|
||||
/// </summary>
|
||||
public partial class MapMultiPolygon : MapPolypoint
|
||||
public IEnumerable<IEnumerable<Location>> Polygons
|
||||
{
|
||||
public static readonly DependencyProperty PolygonsProperty =
|
||||
DependencyPropertyHelper.Register<MapMultiPolygon, IEnumerable<IEnumerable<Location>>>(nameof(Polygons), null,
|
||||
(polygon, oldValue, newValue) => polygon.DataCollectionPropertyChanged(oldValue, newValue));
|
||||
get => (IEnumerable<IEnumerable<Location>>)GetValue(PolygonsProperty);
|
||||
set => SetValue(PolygonsProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Locations that define the multi-polygon points.
|
||||
/// </summary>
|
||||
public IEnumerable<IEnumerable<Location>> Polygons
|
||||
{
|
||||
get => (IEnumerable<IEnumerable<Location>>)GetValue(PolygonsProperty);
|
||||
set => SetValue(PolygonsProperty, value);
|
||||
}
|
||||
|
||||
protected override void UpdateData()
|
||||
{
|
||||
UpdateData(Polygons);
|
||||
}
|
||||
protected override void UpdateData()
|
||||
{
|
||||
UpdateData(Polygons);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,110 +11,109 @@ using Windows.UI.Xaml;
|
|||
using Microsoft.UI.Xaml;
|
||||
#endif
|
||||
|
||||
namespace MapControl
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// A MapPanel with a collection of GroundOverlay or GeoImage children.
|
||||
/// </summary>
|
||||
public partial class MapOverlaysPanel : MapPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// A MapPanel with a collection of GroundOverlay or GeoImage children.
|
||||
/// </summary>
|
||||
public partial class MapOverlaysPanel : MapPanel
|
||||
public static readonly DependencyProperty SourcePathsProperty =
|
||||
DependencyPropertyHelper.Register<MapOverlaysPanel, IEnumerable<string>>(nameof(SourcePaths), null,
|
||||
async (control, oldValue, newValue) => await control.SourcePathsPropertyChanged(oldValue, newValue));
|
||||
|
||||
public IEnumerable<string> SourcePaths
|
||||
{
|
||||
public static readonly DependencyProperty SourcePathsProperty =
|
||||
DependencyPropertyHelper.Register<MapOverlaysPanel, IEnumerable<string>>(nameof(SourcePaths), null,
|
||||
async (control, oldValue, newValue) => await control.SourcePathsPropertyChanged(oldValue, newValue));
|
||||
get => (IEnumerable<string>)GetValue(SourcePathsProperty);
|
||||
set => SetValue(SourcePathsProperty, value);
|
||||
}
|
||||
|
||||
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);
|
||||
set => SetValue(SourcePathsProperty, value);
|
||||
oldCollection.CollectionChanged -= SourcePathsCollectionChanged;
|
||||
}
|
||||
|
||||
private async Task SourcePathsPropertyChanged(IEnumerable<string> oldSourcePaths, IEnumerable<string> newSourcePaths)
|
||||
if (newSourcePaths != null)
|
||||
{
|
||||
Children.Clear();
|
||||
|
||||
if (oldSourcePaths is INotifyCollectionChanged oldCollection)
|
||||
if (newSourcePaths is INotifyCollectionChanged newCollection)
|
||||
{
|
||||
oldCollection.CollectionChanged -= SourcePathsCollectionChanged;
|
||||
newCollection.CollectionChanged += SourcePathsCollectionChanged;
|
||||
}
|
||||
|
||||
if (newSourcePaths != null)
|
||||
{
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,402 +24,401 @@ using Avalonia.Media;
|
|||
/// Arranges child elements on a Map at positions specified by the attached property Location,
|
||||
/// or in rectangles specified by the attached property BoundingBox.
|
||||
/// </summary>
|
||||
namespace MapControl
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// Optional interface to hold the value of the attached property MapPanel.ParentMap.
|
||||
/// </summary>
|
||||
public interface IMapElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional interface to hold the value of the attached property MapPanel.ParentMap.
|
||||
/// </summary>
|
||||
public interface IMapElement
|
||||
MapBase ParentMap { get; set; }
|
||||
}
|
||||
|
||||
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 =
|
||||
DependencyPropertyHelper.RegisterAttached<Point?>("ViewPosition", typeof(MapPanel));
|
||||
get => parentMap;
|
||||
set => SetParentMap(value);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public static bool GetAutoCollapse(FrameworkElement element)
|
||||
{
|
||||
return (bool)element.GetValue(AutoCollapseProperty);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
FlowDirection = FlowDirection.LeftToRight;
|
||||
SetValue(ParentMapProperty, this);
|
||||
}
|
||||
#if UWP || WINUI
|
||||
else
|
||||
{
|
||||
InitMapElement(this);
|
||||
}
|
||||
#endif
|
||||
parentMap.ViewportChanged -= OnViewportChanged;
|
||||
}
|
||||
|
||||
private MapBase parentMap;
|
||||
parentMap = map;
|
||||
|
||||
/// <summary>
|
||||
/// Implements IMapElement.ParentMap.
|
||||
/// </summary>
|
||||
public MapBase ParentMap
|
||||
if (parentMap != null && parentMap != this)
|
||||
{
|
||||
get => parentMap;
|
||||
set => SetParentMap(value);
|
||||
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>())
|
||||
{
|
||||
element.Measure(availableSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public static bool GetAutoCollapse(FrameworkElement element)
|
||||
return new Size();
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
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>())
|
||||
{
|
||||
element.Measure(availableSize);
|
||||
ArrangeChildElement(element, finalSize);
|
||||
}
|
||||
|
||||
return new Size();
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
if (parentMap != null)
|
||||
{
|
||||
foreach (var element in Children.Cast<FrameworkElement>())
|
||||
{
|
||||
ArrangeChildElement(element, finalSize);
|
||||
}
|
||||
}
|
||||
return 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);
|
||||
|
||||
if (!location.LongitudeEquals(longitude))
|
||||
{
|
||||
position = parentMap.LocationToView(location.Latitude, 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 = parentMap.ViewTransform.MapToView(center);
|
||||
var position = SetViewPosition(element, GetViewPosition(location));
|
||||
|
||||
if (parentMap.MapProjection.IsNormalCylindrical && !parentMap.InsideViewBounds(position))
|
||||
if (GetAutoCollapse(element))
|
||||
{
|
||||
var location = parentMap.MapProjection.MapToLocation(center);
|
||||
var longitude = parentMap.NearestLongitude(location.Longitude);
|
||||
|
||||
if (!location.LongitudeEquals(longitude))
|
||||
{
|
||||
position = parentMap.LocationToView(location.Latitude, longitude);
|
||||
}
|
||||
element.SetVisible(parentMap.InsideViewBounds(position));
|
||||
}
|
||||
|
||||
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);
|
||||
ArrangeElement(element, position);
|
||||
}
|
||||
|
||||
private void ArrangeChildElement(FrameworkElement element, Size panelSize)
|
||||
else
|
||||
{
|
||||
var location = GetLocation(element);
|
||||
element.ClearValue(ViewPositionProperty);
|
||||
|
||||
if (location != null)
|
||||
var mapRect = GetMapRect(element);
|
||||
|
||||
if (mapRect.HasValue)
|
||||
{
|
||||
var position = SetViewPosition(element, GetViewPosition(location));
|
||||
|
||||
if (GetAutoCollapse(element))
|
||||
{
|
||||
element.SetVisible(parentMap.InsideViewBounds(position));
|
||||
}
|
||||
|
||||
ArrangeElement(element, position);
|
||||
ArrangeElement(element, mapRect.Value, 0d);
|
||||
}
|
||||
else
|
||||
{
|
||||
element.ClearValue(ViewPositionProperty);
|
||||
var boundingBox = GetBoundingBox(element);
|
||||
|
||||
var mapRect = GetMapRect(element);
|
||||
|
||||
if (mapRect.HasValue)
|
||||
if (boundingBox != null)
|
||||
{
|
||||
ArrangeElement(element, mapRect.Value, 0d);
|
||||
(var rect, var rotation) = parentMap.MapProjection.BoundingBoxToMap(boundingBox);
|
||||
|
||||
ArrangeElement(element, rect, -rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
var boundingBox = GetBoundingBox(element);
|
||||
|
||||
if (boundingBox != null)
|
||||
{
|
||||
(var rect, var rotation) = parentMap.MapProjection.BoundingBoxToMap(boundingBox);
|
||||
|
||||
ArrangeElement(element, rect, -rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
ArrangeElement(element, panelSize);
|
||||
}
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
rotateTransform.Angle = rotation;
|
||||
}
|
||||
|
||||
private static void ArrangeElement(FrameworkElement element, Point position)
|
||||
else if (rotation != 0d)
|
||||
{
|
||||
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);
|
||||
element.SetRenderTransform(new RotateTransform { Angle = rotation }, true);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
#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>
|
||||
/// 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.
|
||||
/// Gets or sets a Location that is either used as
|
||||
/// - 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>
|
||||
public partial class MapPath : IMapElement
|
||||
public Location Location
|
||||
{
|
||||
public static readonly DependencyProperty LocationProperty =
|
||||
DependencyPropertyHelper.Register<MapPath, Location>(nameof(Location), null,
|
||||
(path, oldValue, newValue) => path.UpdateData());
|
||||
get => (Location)GetValue(LocationProperty);
|
||||
set => SetValue(LocationProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a Location that is either used as
|
||||
/// - 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>
|
||||
public Location Location
|
||||
/// <summary>
|
||||
/// Implements IMapElement.ParentMap.
|
||||
/// </summary>
|
||||
public MapBase ParentMap
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
get => (Location)GetValue(LocationProperty);
|
||||
set => SetValue(LocationProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements IMapElement.ParentMap.
|
||||
/// </summary>
|
||||
public MapBase ParentMap
|
||||
{
|
||||
get;
|
||||
set
|
||||
if (field != null)
|
||||
{
|
||||
if (field != null)
|
||||
{
|
||||
field.ViewportChanged -= OnViewportChanged;
|
||||
}
|
||||
|
||||
field = value;
|
||||
|
||||
if (field != null)
|
||||
{
|
||||
field.ViewportChanged += OnViewportChanged;
|
||||
}
|
||||
|
||||
UpdateData();
|
||||
field.ViewportChanged -= OnViewportChanged;
|
||||
}
|
||||
|
||||
field = value;
|
||||
|
||||
if (field != null)
|
||||
{
|
||||
field.ViewportChanged += OnViewportChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
||||
{
|
||||
UpdateData();
|
||||
}
|
||||
}
|
||||
|
||||
protected void SetDataTransform(Matrix matrix)
|
||||
{
|
||||
if (Data.Transform is MatrixTransform transform
|
||||
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
||||
{
|
||||
UpdateData();
|
||||
}
|
||||
|
||||
protected void SetDataTransform(Matrix matrix)
|
||||
{
|
||||
if (Data.Transform is MatrixTransform transform
|
||||
#if WPF
|
||||
&& !transform.IsFrozen
|
||||
&& !transform.IsFrozen
|
||||
#endif
|
||||
)
|
||||
{
|
||||
transform.Matrix = matrix;
|
||||
}
|
||||
else
|
||||
{
|
||||
Data.Transform = new MatrixTransform { Matrix = matrix };
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateData()
|
||||
)
|
||||
{
|
||||
if (Data != null && ParentMap != null && Location != null)
|
||||
{
|
||||
SetDataTransform(ParentMap.GetMapToViewTransform(Location));
|
||||
}
|
||||
|
||||
MapPanel.SetLocation(this, Location);
|
||||
transform.Matrix = matrix;
|
||||
}
|
||||
|
||||
protected Point LocationToMap(Location location, double longitudeOffset)
|
||||
else
|
||||
{
|
||||
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));
|
||||
Data.Transform = new MatrixTransform { Matrix = matrix };
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
#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>
|
||||
/// A polygon defined by a collection of Locations.
|
||||
/// Gets or sets the Locations that define the polygon points.
|
||||
/// </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
|
||||
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
||||
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
||||
#endif
|
||||
public IEnumerable<Location> Locations
|
||||
{
|
||||
get => (IEnumerable<Location>)GetValue(LocationsProperty);
|
||||
set => SetValue(LocationsProperty, value);
|
||||
}
|
||||
public IEnumerable<Location> Locations
|
||||
{
|
||||
get => (IEnumerable<Location>)GetValue(LocationsProperty);
|
||||
set => SetValue(LocationsProperty, value);
|
||||
}
|
||||
|
||||
protected override void UpdateData()
|
||||
{
|
||||
UpdateData(Locations, true);
|
||||
}
|
||||
protected override void UpdateData()
|
||||
{
|
||||
UpdateData(Locations, true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,32 +7,31 @@ using Windows.UI.Xaml;
|
|||
using Microsoft.UI.Xaml;
|
||||
#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>
|
||||
/// A polyline defined by a collection of Locations.
|
||||
/// Gets or sets the Locations that define the polyline points.
|
||||
/// </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
|
||||
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
||||
[System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))]
|
||||
#endif
|
||||
public IEnumerable<Location> Locations
|
||||
{
|
||||
get => (IEnumerable<Location>)GetValue(LocationsProperty);
|
||||
set => SetValue(LocationsProperty, value);
|
||||
}
|
||||
public IEnumerable<Location> Locations
|
||||
{
|
||||
get => (IEnumerable<Location>)GetValue(LocationsProperty);
|
||||
set => SetValue(LocationsProperty, value);
|
||||
}
|
||||
|
||||
protected override void UpdateData()
|
||||
{
|
||||
UpdateData(Locations, false);
|
||||
}
|
||||
protected override void UpdateData()
|
||||
{
|
||||
UpdateData(Locations, false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,64 +20,63 @@ using Avalonia.Media;
|
|||
using PolypointGeometry = Avalonia.Media.PathGeometry;
|
||||
#endif
|
||||
|
||||
namespace MapControl
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// Base class of MapPolyline and MapPolygon and MapMultiPolygon.
|
||||
/// </summary>
|
||||
public partial class MapPolypoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class of MapPolyline and MapPolygon and MapMultiPolygon.
|
||||
/// </summary>
|
||||
public partial class MapPolypoint
|
||||
public static readonly DependencyProperty FillRuleProperty =
|
||||
DependencyPropertyHelper.Register<MapPolygon, FillRule>(nameof(FillRule), FillRule.EvenOdd,
|
||||
(polypoint, oldValue, newValue) => ((PolypointGeometry)polypoint.Data).FillRule = newValue);
|
||||
|
||||
public FillRule FillRule
|
||||
{
|
||||
public static readonly DependencyProperty FillRuleProperty =
|
||||
DependencyPropertyHelper.Register<MapPolygon, FillRule>(nameof(FillRule), FillRule.EvenOdd,
|
||||
(polypoint, oldValue, newValue) => ((PolypointGeometry)polypoint.Data).FillRule = newValue);
|
||||
get => (FillRule)GetValue(FillRuleProperty);
|
||||
set => SetValue(FillRuleProperty, value);
|
||||
}
|
||||
|
||||
public FillRule FillRule
|
||||
protected MapPolypoint()
|
||||
{
|
||||
Data = new PolypointGeometry();
|
||||
}
|
||||
|
||||
protected void DataCollectionPropertyChanged(IEnumerable oldValue, IEnumerable newValue)
|
||||
{
|
||||
if (oldValue is INotifyCollectionChanged oldCollection)
|
||||
{
|
||||
get => (FillRule)GetValue(FillRuleProperty);
|
||||
set => SetValue(FillRuleProperty, value);
|
||||
oldCollection.CollectionChanged -= DataCollectionChanged;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
return longitudeOffset;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,134 +6,133 @@ using System.Windows.Media;
|
|||
using Avalonia;
|
||||
#endif
|
||||
|
||||
namespace MapControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a map projection, a transformation between geographic coordinates,
|
||||
/// i.e. latitude and longitude in degrees, and cartesian map coordinates in meters.
|
||||
/// See https://en.wikipedia.org/wiki/Map_projection.
|
||||
/// </summary>
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a map projection, a transformation between geographic coordinates,
|
||||
/// i.e. latitude and longitude in degrees, and cartesian map coordinates in meters.
|
||||
/// See https://en.wikipedia.org/wiki/Map_projection.
|
||||
/// </summary>
|
||||
#if UWP || WINUI
|
||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||
#else
|
||||
[System.ComponentModel.TypeConverter(typeof(MapProjectionConverter))]
|
||||
[System.ComponentModel.TypeConverter(typeof(MapProjectionConverter))]
|
||||
#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;
|
||||
public const double Wgs84Flattening = 1d / 298.257223563;
|
||||
public const double Wgs84MeterPerDegree = Wgs84EquatorialRadius * Math.PI / 180d;
|
||||
get => field ??= new MapProjectionFactory();
|
||||
set;
|
||||
}
|
||||
|
||||
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();
|
||||
set;
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
return (rect, rotation);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +1,56 @@
|
|||
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);
|
||||
|
||||
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.");
|
||||
projection = CreateProjection(epsgCode);
|
||||
}
|
||||
|
||||
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(),
|
||||
Wgs84UpsNorthProjection.DefaultCrsId => new Wgs84UpsNorthProjection(),
|
||||
Wgs84UpsSouthProjection.DefaultCrsId => new Wgs84UpsSouthProjection(),
|
||||
EquirectangularProjection.DefaultCrsId or "CRS:84" => new EquirectangularProjection(crsId),
|
||||
_ => null
|
||||
};
|
||||
WebMercatorProjection.DefaultCrsId => new WebMercatorProjection(),
|
||||
WorldMercatorProjection.DefaultCrsId => new WorldMercatorProjection(),
|
||||
Wgs84UpsNorthProjection.DefaultCrsId => new Wgs84UpsNorthProjection(),
|
||||
Wgs84UpsSouthProjection.DefaultCrsId => new Wgs84UpsSouthProjection(),
|
||||
EquirectangularProjection.DefaultCrsId or "CRS:84" => new EquirectangularProjection(crsId),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (projection == null && crsId.StartsWith(StereographicProjection.DefaultCrsId))
|
||||
{
|
||||
projection = new StereographicProjection(crsId);
|
||||
}
|
||||
|
||||
return projection;
|
||||
if (projection == null && crsId.StartsWith(StereographicProjection.DefaultCrsId))
|
||||
{
|
||||
projection = new StereographicProjection(crsId);
|
||||
}
|
||||
|
||||
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 >= Nad83UtmProjection.FirstZoneEpsgCode
|
||||
and <= Nad83UtmProjection.LastZoneEpsgCode => new Nad83UtmProjection(c % 100),
|
||||
var c when c is >= Wgs84UtmProjection.FirstZoneNorthEpsgCode
|
||||
and <= Wgs84UtmProjection.LastZoneNorthEpsgCode => new Wgs84UtmProjection(c % 100, true),
|
||||
var c when c is >= Wgs84UtmProjection.FirstZoneSouthEpsgCode
|
||||
and <= Wgs84UtmProjection.LastZoneSouthEpsgCode => new Wgs84UtmProjection(c % 100, false),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
var c when c is >= Etrs89UtmProjection.FirstZoneEpsgCode
|
||||
and <= Etrs89UtmProjection.LastZoneEpsgCode => new Etrs89UtmProjection(c % 100),
|
||||
var c when c is >= Nad83UtmProjection.FirstZoneEpsgCode
|
||||
and <= Nad83UtmProjection.LastZoneEpsgCode => new Nad83UtmProjection(c % 100),
|
||||
var c when c is >= Wgs84UtmProjection.FirstZoneNorthEpsgCode
|
||||
and <= Wgs84UtmProjection.LastZoneNorthEpsgCode => new Wgs84UtmProjection(c % 100, true),
|
||||
var c when c is >= Wgs84UtmProjection.FirstZoneSouthEpsgCode
|
||||
and <= Wgs84UtmProjection.LastZoneSouthEpsgCode => new Wgs84UtmProjection(c % 100, false),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,105 +26,104 @@ using Avalonia.Layout;
|
|||
using PropertyPath = System.String;
|
||||
#endif
|
||||
|
||||
namespace MapControl
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// Draws a map scale overlay.
|
||||
/// </summary>
|
||||
public partial class MapScale : MapPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// Draws a map scale overlay.
|
||||
/// </summary>
|
||||
public partial class MapScale : MapPanel
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
DependencyPropertyHelper.Register<MapScale, Thickness>(nameof(Padding), new Thickness(4));
|
||||
|
||||
public static readonly DependencyProperty StrokeThicknessProperty =
|
||||
DependencyPropertyHelper.Register<MapScale, double>(nameof(StrokeThickness), 1d);
|
||||
|
||||
public Thickness Padding
|
||||
{
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
DependencyPropertyHelper.Register<MapScale, Thickness>(nameof(Padding), new Thickness(4));
|
||||
get => (Thickness)GetValue(PaddingProperty);
|
||||
set => SetValue(PaddingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty StrokeThicknessProperty =
|
||||
DependencyPropertyHelper.Register<MapScale, double>(nameof(StrokeThickness), 1d);
|
||||
public double StrokeThickness
|
||||
{
|
||||
get => (double)GetValue(StrokeThicknessProperty);
|
||||
set => SetValue(StrokeThicknessProperty, value);
|
||||
}
|
||||
|
||||
public Thickness Padding
|
||||
{
|
||||
get => (Thickness)GetValue(PaddingProperty);
|
||||
set => SetValue(PaddingProperty, value);
|
||||
}
|
||||
private readonly Polyline line = new Polyline();
|
||||
|
||||
public double StrokeThickness
|
||||
{
|
||||
get => (double)GetValue(StrokeThicknessProperty);
|
||||
set => SetValue(StrokeThicknessProperty, value);
|
||||
}
|
||||
private readonly TextBlock label = new TextBlock
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
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
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
protected override void SetParentMap(MapBase map)
|
||||
{
|
||||
base.SetParentMap(map);
|
||||
|
||||
public MapScale()
|
||||
{
|
||||
MinWidth = 100d;
|
||||
Children.Add(line);
|
||||
Children.Add(label);
|
||||
}
|
||||
line.SetBinding(Shape.StrokeThicknessProperty,
|
||||
new Binding { Source = this, Path = new PropertyPath(nameof(StrokeThickness)) });
|
||||
|
||||
protected override void SetParentMap(MapBase map)
|
||||
{
|
||||
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)) });
|
||||
line.SetBinding(Shape.StrokeProperty,
|
||||
new Binding { Source = map, Path = new PropertyPath(nameof(MapBase.Foreground)) });
|
||||
|
||||
#if UWP || WINUI
|
||||
label.SetBinding(TextBlock.ForegroundProperty,
|
||||
new Binding { Source = map, Path = new PropertyPath(nameof(MapBase.Foreground)) });
|
||||
label.SetBinding(TextBlock.ForegroundProperty,
|
||||
new Binding { Source = map, Path = new PropertyPath(nameof(MapBase.Foreground)) });
|
||||
#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)
|
||||
{
|
||||
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)));
|
||||
length = length / magnitude < 2d ? 2d * magnitude
|
||||
: length / magnitude < 5d ? 5d * magnitude
|
||||
: 10d * magnitude;
|
||||
|
||||
length = length / magnitude < 2d ? 2d * magnitude
|
||||
: length / magnitude < 5d ? 5d * magnitude
|
||||
: 10d * magnitude;
|
||||
size = new Size(
|
||||
length * scale + StrokeThickness + Padding.Left + Padding.Right,
|
||||
1.5 * label.FontSize + 2 * StrokeThickness + Padding.Top + Padding.Bottom);
|
||||
|
||||
size = new Size(
|
||||
length * scale + StrokeThickness + Padding.Left + Padding.Right,
|
||||
1.5 * label.FontSize + 2 * StrokeThickness + Padding.Top + Padding.Bottom);
|
||||
var x1 = Padding.Left + StrokeThickness / 2d;
|
||||
var x2 = size.Width - Padding.Right - StrokeThickness / 2d;
|
||||
var y1 = size.Height / 2d;
|
||||
var y2 = size.Height - Padding.Bottom - StrokeThickness / 2d;
|
||||
|
||||
var x1 = Padding.Left + StrokeThickness / 2d;
|
||||
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)];
|
||||
|
||||
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
|
||||
? string.Format(CultureInfo.InvariantCulture, "{0:F0} km", length / 1000d)
|
||||
: string.Format(CultureInfo.InvariantCulture, "{0:F0} m", length);
|
||||
|
||||
line.Measure(size);
|
||||
label.Measure(size);
|
||||
}
|
||||
|
||||
return size;
|
||||
line.Measure(size);
|
||||
label.Measure(size);
|
||||
}
|
||||
|
||||
protected override void OnViewportChanged(ViewportChangedEventArgs e)
|
||||
{
|
||||
InvalidateMeasure();
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
protected override void OnViewportChanged(ViewportChangedEventArgs e)
|
||||
{
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,226 +16,225 @@ using Avalonia;
|
|||
using Avalonia.Media;
|
||||
#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>
|
||||
/// Displays a Web Mercator tile pyramid.
|
||||
/// A default MapTileLayer using OpenStreetMap data.
|
||||
/// </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,
|
||||
180d * MapProjection.Wgs84MeterPerDegree);
|
||||
public MapTileLayer()
|
||||
{
|
||||
this.SetRenderTransform(new MatrixTransform());
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TileSourceProperty =
|
||||
DependencyPropertyHelper.Register<MapTileLayer, TileSource>(nameof(TileSource), null,
|
||||
(layer, oldValue, newValue) => layer.UpdateTileCollection(true));
|
||||
public TileMatrix TileMatrix { get; private set; }
|
||||
|
||||
public static readonly DependencyProperty MinZoomLevelProperty =
|
||||
DependencyPropertyHelper.Register<MapTileLayer, int>(nameof(MinZoomLevel), 0);
|
||||
public ICollection<ImageTile> Tiles { get; private set; } = [];
|
||||
|
||||
public static readonly DependencyProperty MaxZoomLevelProperty =
|
||||
DependencyPropertyHelper.Register<MapTileLayer, int>(nameof(MaxZoomLevel), 19);
|
||||
/// <summary>
|
||||
/// 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 =
|
||||
DependencyPropertyHelper.Register<MapTileLayer, double>(nameof(ZoomLevelOffset), 0d);
|
||||
/// <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>
|
||||
/// A default MapTileLayer using OpenStreetMap data.
|
||||
/// </summary>
|
||||
public static MapTileLayer OpenStreetMapTileLayer => new MapTileLayer
|
||||
/// <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)
|
||||
{
|
||||
TileSource = TileSource.Parse("https://tile.openstreetmap.org/{z}/{x}/{y}.png"),
|
||||
SourceName = "OpenStreetMap",
|
||||
Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)"
|
||||
};
|
||||
|
||||
public MapTileLayer()
|
||||
{
|
||||
this.SetRenderTransform(new MatrixTransform());
|
||||
tile.Image.Measure(availableSize);
|
||||
}
|
||||
|
||||
public TileMatrix TileMatrix { get; private set; }
|
||||
return new Size();
|
||||
}
|
||||
|
||||
public ICollection<ImageTile> Tiles { get; private set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Provides the ImagesSource or image request Uri for map tiles.
|
||||
/// </summary>
|
||||
public TileSource TileSource
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
foreach (var tile in Tiles)
|
||||
{
|
||||
get => (TileSource)GetValue(TileSourceProperty);
|
||||
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.
|
||||
// Arrange tiles relative to TileMatrix.XMin/YMin.
|
||||
//
|
||||
var tileMatrixZoomLevel = (int)Math.Floor(ParentMap.ZoomLevel - ZoomLevelOffset + 0.001);
|
||||
var tileMatrixScale = MapBase.ZoomLevelToScale(tileMatrixZoomLevel);
|
||||
var tileSize = TileSize << (TileMatrix.ZoomLevel - tile.ZoomLevel);
|
||||
var x = tileSize * tile.X - TileSize * TileMatrix.XMin;
|
||||
var y = tileSize * tile.Y - TileSize * TileMatrix.YMin;
|
||||
|
||||
// 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;
|
||||
tile.Image.Width = tileSize;
|
||||
tile.Image.Height = tileSize;
|
||||
tile.Image.Arrange(new Rect(x, y, tileSize, tileSize));
|
||||
}
|
||||
|
||||
private void UpdateTiles()
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
protected override void UpdateRenderTransform()
|
||||
{
|
||||
if (TileMatrix != null)
|
||||
{
|
||||
var tiles = new ImageTileList();
|
||||
var maxZoomLevel = Math.Min(TileMatrix.ZoomLevel, MaxZoomLevel);
|
||||
// Tile matrix origin in pixels.
|
||||
//
|
||||
var tileMatrixOrigin = new Point(TileSize * TileMatrix.XMin, TileSize * TileMatrix.YMin);
|
||||
var tileMatrixScale = MapBase.ZoomLevelToScale(TileMatrix.ZoomLevel);
|
||||
|
||||
if (maxZoomLevel >= MinZoomLevel)
|
||||
{
|
||||
var minZoomLevel = maxZoomLevel;
|
||||
((MatrixTransform)RenderTransform).Matrix =
|
||||
ParentMap.ViewTransform.GetTileLayerTransform(tileMatrixScale, MapTopLeft, tileMatrixOrigin);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
protected override void UpdateTileCollection()
|
||||
{
|
||||
UpdateTileCollection(false);
|
||||
}
|
||||
|
||||
private void UpdateTileCollection(bool tileSourceChanged)
|
||||
{
|
||||
if (TileSource == null || ParentMap?.MapProjection.CrsId != WebMercatorProjection.DefaultCrsId)
|
||||
{
|
||||
CancelLoadTiles();
|
||||
Children.Clear();
|
||||
|
||||
foreach (var tile in tiles)
|
||||
Tiles.Clear();
|
||||
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;
|
||||
#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>
|
||||
/// 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)
|
||||
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)
|
||||
{
|
||||
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
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
var cos = Math.Cos(angle);
|
||||
var sin = Math.Sin(angle);
|
||||
|
||||
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;
|
||||
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(
|
||||
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;
|
||||
#endif
|
||||
|
||||
namespace MapControl
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// Draws a metric grid overlay.
|
||||
/// </summary>
|
||||
public partial class MetricGrid : MapGrid
|
||||
{
|
||||
/// <summary>
|
||||
/// Draws a metric grid overlay.
|
||||
/// </summary>
|
||||
public partial class MetricGrid : MapGrid
|
||||
protected override void DrawGrid(PathFigureCollection figures, List<Label> labels)
|
||||
{
|
||||
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);
|
||||
var lineDistance = Math.Pow(10d, Math.Ceiling(Math.Log10(minLineDistance)));
|
||||
lineDistance *= 0.5;
|
||||
|
||||
if (lineDistance * 0.5 >= minLineDistance)
|
||||
if (lineDistance * 0.4 >= minLineDistance)
|
||||
{
|
||||
lineDistance *= 0.5;
|
||||
|
||||
if (lineDistance * 0.4 >= minLineDistance)
|
||||
{
|
||||
lineDistance *= 0.4;
|
||||
}
|
||||
lineDistance *= 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
var mapRect = ParentMap.ViewTransform.ViewToMapBounds(new Rect(0d, 0d, ParentMap.ActualWidth, ParentMap.ActualHeight));
|
||||
var minX = Math.Ceiling(mapRect.X / lineDistance) * lineDistance;
|
||||
var minY = Math.Ceiling(mapRect.Y / lineDistance) * lineDistance;
|
||||
var mapRect = ParentMap.ViewTransform.ViewToMapBounds(new Rect(0d, 0d, ParentMap.ActualWidth, ParentMap.ActualHeight));
|
||||
var minX = Math.Ceiling(mapRect.X / lineDistance) * lineDistance;
|
||||
var minY = Math.Ceiling(mapRect.Y / lineDistance) * lineDistance;
|
||||
|
||||
for (var x = minX; x <= mapRect.X + mapRect.Width; x += lineDistance)
|
||||
{
|
||||
var p1 = ParentMap.ViewTransform.MapToView(new Point(x, mapRect.Y));
|
||||
var p2 = ParentMap.ViewTransform.MapToView(new Point(x, mapRect.Y + mapRect.Height));
|
||||
figures.Add(CreateLineFigure(p1, p2));
|
||||
for (var x = minX; x <= mapRect.X + mapRect.Width; x += lineDistance)
|
||||
{
|
||||
var p1 = ParentMap.ViewTransform.MapToView(new Point(x, mapRect.Y));
|
||||
var p2 = ParentMap.ViewTransform.MapToView(new Point(x, mapRect.Y + mapRect.Height));
|
||||
figures.Add(CreateLineFigure(p1, p2));
|
||||
|
||||
var text = x.ToString("F0");
|
||||
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));
|
||||
}
|
||||
var text = x.ToString("F0");
|
||||
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));
|
||||
}
|
||||
|
||||
for (var y = minY; y <= mapRect.Y + mapRect.Height; y += lineDistance)
|
||||
{
|
||||
var p1 = ParentMap.ViewTransform.MapToView(new Point(mapRect.X, y));
|
||||
var p2 = ParentMap.ViewTransform.MapToView(new Point(mapRect.X + mapRect.Width, y));
|
||||
figures.Add(CreateLineFigure(p1, p2));
|
||||
for (var y = minY; y <= mapRect.Y + mapRect.Height; y += lineDistance)
|
||||
{
|
||||
var p1 = ParentMap.ViewTransform.MapToView(new Point(mapRect.X, y));
|
||||
var p2 = ParentMap.ViewTransform.MapToView(new Point(mapRect.X + mapRect.Width, y));
|
||||
figures.Add(CreateLineFigure(p1, p2));
|
||||
|
||||
var text = y.ToString("F0");
|
||||
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));
|
||||
}
|
||||
var text = y.ToString("F0");
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,120 +6,119 @@ using System.Windows.Media;
|
|||
using Avalonia;
|
||||
#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>
|
||||
/// 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)
|
||||
{
|
||||
public override double GridConvergence(double latitude, double longitude)
|
||||
{
|
||||
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);
|
||||
}
|
||||
return Math.Sign(LatitudeOfOrigin) * (longitude - CentralMeridian);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Universal Polar Stereographic North Projection - EPSG:32661.
|
||||
/// </summary>
|
||||
public class Wgs84UpsNorthProjection : PolarStereographicProjection
|
||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||
{
|
||||
public const string DefaultCrsId = "EPSG:32661";
|
||||
var sign = Math.Sign(LatitudeOfOrigin);
|
||||
var phi = sign * latitude * Math.PI / 180d;
|
||||
|
||||
public Wgs84UpsNorthProjection() // parameterless constructor for XAML
|
||||
: this(DefaultCrsId)
|
||||
{
|
||||
}
|
||||
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)
|
||||
|
||||
public Wgs84UpsNorthProjection(string crsId)
|
||||
{
|
||||
CrsId = crsId;
|
||||
ScaleFactor = 0.994;
|
||||
LatitudeOfOrigin = 90d;
|
||||
FalseEasting = 2e6;
|
||||
FalseNorthing = 2e6;
|
||||
}
|
||||
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
|
||||
transform.Rotate(-sign * (longitude - CentralMeridian));
|
||||
|
||||
return transform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Universal Polar Stereographic South Projection - EPSG:32761.
|
||||
/// </summary>
|
||||
public class Wgs84UpsSouthProjection : PolarStereographicProjection
|
||||
public override Point LocationToMap(double latitude, double longitude)
|
||||
{
|
||||
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
|
||||
: this(DefaultCrsId)
|
||||
{
|
||||
}
|
||||
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)
|
||||
|
||||
public Wgs84UpsSouthProjection(string crsId)
|
||||
{
|
||||
CrsId = crsId;
|
||||
ScaleFactor = 0.994;
|
||||
LatitudeOfOrigin = -90d;
|
||||
FalseEasting = 2e6;
|
||||
FalseNorthing = 2e6;
|
||||
}
|
||||
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>
|
||||
/// 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.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>
|
||||
/// 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (polygon is INotifyCollectionChanged addedPolygon)
|
||||
{
|
||||
addedPolygon.CollectionChanged += PolygonChanged;
|
||||
}
|
||||
base.InsertItem(index, polygon);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
removedPolygon.CollectionChanged -= PolygonChanged;
|
||||
}
|
||||
|
||||
if (polygon is INotifyCollectionChanged addedPolygon)
|
||||
{
|
||||
addedPolygon.CollectionChanged += PolygonChanged;
|
||||
}
|
||||
|
||||
base.SetItem(index, polygon);
|
||||
addedPolygon.CollectionChanged += PolygonChanged;
|
||||
}
|
||||
|
||||
protected override void RemoveItem(int index)
|
||||
{
|
||||
if (this[index] is INotifyCollectionChanged removedPolygon)
|
||||
{
|
||||
removedPolygon.CollectionChanged -= PolygonChanged;
|
||||
}
|
||||
base.SetItem(index, polygon);
|
||||
}
|
||||
|
||||
base.RemoveItem(index);
|
||||
protected override void RemoveItem(int index)
|
||||
{
|
||||
if (this[index] is INotifyCollectionChanged removedPolygon)
|
||||
{
|
||||
removedPolygon.CollectionChanged -= PolygonChanged;
|
||||
}
|
||||
|
||||
protected override void ClearItems()
|
||||
{
|
||||
foreach (var polygon in this.OfType<INotifyCollectionChanged>())
|
||||
{
|
||||
polygon.CollectionChanged -= PolygonChanged;
|
||||
}
|
||||
base.RemoveItem(index);
|
||||
}
|
||||
|
||||
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;
|
||||
#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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
get => (Size)GetValue(ArrowSizeProperty);
|
||||
set => SetValue(ArrowSizeProperty, value);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
Point = new Point(x, y)
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
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),
|
||||
SweepDirection = SweepDirection.Clockwise
|
||||
});
|
||||
}
|
||||
Point = new Point(x, y),
|
||||
Size = new Size(r, r),
|
||||
SweepDirection = SweepDirection.Clockwise
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,136 +7,135 @@ using System.Windows.Media;
|
|||
using Avalonia;
|
||||
#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>
|
||||
/// 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
|
||||
public const string DefaultCrsId = "AUTO2:97002"; // GeoServer non-standard CRS identifier
|
||||
|
||||
public StereographicProjection(string crsId)
|
||||
{
|
||||
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(',');
|
||||
|
||||
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;
|
||||
throw new ArgumentException($"Invalid CRS Identifier {crsId}.", nameof(crsId));
|
||||
}
|
||||
|
||||
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,
|
||||
"{0},{1:0.########},{2:0.########},{3:0.########}", crsId, scaleFactor, centerLongitude, centerLatitude);
|
||||
ScaleFactor = scaleFactor;
|
||||
CentralMeridian = centerLongitude;
|
||||
LatitudeOfOrigin = centerLatitude;
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
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;
|
||||
#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 Y => y;
|
||||
public int Row => y;
|
||||
public int Column { get; } = ((x % columnCount) + columnCount) % columnCount;
|
||||
public int ZoomLevel => zoomLevel;
|
||||
public int X => x;
|
||||
public int Y => y;
|
||||
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>
|
||||
/// Runs a tile image download Task and passes the result to the UI thread.
|
||||
/// </summary>
|
||||
public abstract Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc);
|
||||
}
|
||||
/// <summary>
|
||||
/// Runs a tile image download Task and passes the result to the UI thread.
|
||||
/// </summary>
|
||||
public abstract Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,225 +8,224 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MapControl
|
||||
namespace MapControl;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and optionally caches map tile images for a MapTilePyramidLayer.
|
||||
/// </summary>
|
||||
public interface ITileImageLoader
|
||||
{
|
||||
/// <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>
|
||||
public interface ITileImageLoader
|
||||
{
|
||||
/// <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);
|
||||
void BeginLoadTiles(IEnumerable<Tile> tiles, TileSource tileSource, string cacheName, IProgress<double> progress);
|
||||
|
||||
/// <summary>
|
||||
/// Terminates all running tile loading tasks.
|
||||
/// </summary>
|
||||
void CancelLoadTiles();
|
||||
/// <summary>
|
||||
/// Terminates all running tile loading tasks.
|
||||
/// </summary>
|
||||
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));
|
||||
|
||||
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)
|
||||
lock (tileQueue)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
tileQueue.Clear();
|
||||
tileCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelLoadTiles()
|
||||
private async Task LoadTilesFromQueue(TileSource tileSource, string cacheName, IProgress<double> progress)
|
||||
{
|
||||
bool TryDequeueTile(out Tile tile)
|
||||
{
|
||||
lock (tileQueue)
|
||||
{
|
||||
tileQueue.Clear();
|
||||
tileCount = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
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 YMin => yMin;
|
||||
public int XMax => xMax;
|
||||
public int YMax => yMax;
|
||||
public int Width => xMax - xMin + 1;
|
||||
public int Height => yMax - yMin + 1;
|
||||
}
|
||||
public int ZoomLevel => zoomLevel;
|
||||
public int XMin => xMin;
|
||||
public int YMin => yMin;
|
||||
public int XMax => xMax;
|
||||
public int YMax => yMax;
|
||||
public int Width => xMax - xMin + 1;
|
||||
public int Height => yMax - yMin + 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,188 +21,187 @@ using Avalonia.Controls;
|
|||
using Brush = Avalonia.Media.IBrush;
|
||||
#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 =
|
||||
DependencyPropertyHelper.Register<TilePyramidLayer, string>(nameof(SourceName));
|
||||
IsHitTestVisible = false;
|
||||
|
||||
public static readonly DependencyProperty DescriptionProperty =
|
||||
DependencyPropertyHelper.Register<TilePyramidLayer, string>(nameof(Description));
|
||||
loadingProgress = new Progress<double>(p => SetValue(LoadingProgressProperty, p));
|
||||
|
||||
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()
|
||||
{
|
||||
IsHitTestVisible = false;
|
||||
|
||||
loadingProgress = new Progress<double>(p => SetValue(LoadingProgressProperty, p));
|
||||
|
||||
updateTimer = new UpdateTimer { Interval = UpdateInterval };
|
||||
updateTimer.Tick += (_, _) => UpdateTiles();
|
||||
updateTimer = new UpdateTimer { Interval = UpdateInterval };
|
||||
updateTimer.Tick += (_, _) => UpdateTiles();
|
||||
#if WPF
|
||||
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
|
||||
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
|
||||
#elif UWP || WINUI
|
||||
ElementCompositionPreview.GetElementVisual(this).BorderMode = CompositionBorderMode.Hard;
|
||||
MapPanel.InitMapElement(this);
|
||||
ElementCompositionPreview.GetElementVisual(this).BorderMode = CompositionBorderMode.Hard;
|
||||
MapPanel.InitMapElement(this);
|
||||
#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();
|
||||
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)
|
||||
{
|
||||
if (field != null)
|
||||
{
|
||||
field.ViewportChanged -= OnViewportChanged;
|
||||
}
|
||||
|
||||
field = value;
|
||||
|
||||
if (field != null)
|
||||
{
|
||||
field.ViewportChanged += OnViewportChanged;
|
||||
}
|
||||
|
||||
updateTimer.Run();
|
||||
field.ViewportChanged -= OnViewportChanged;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
if (field != null)
|
||||
{
|
||||
UpdateTiles(); // update immediately
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateRenderTransform();
|
||||
updateTimer.Run(!UpdateWhileViewportChanging);
|
||||
field.ViewportChanged += OnViewportChanged;
|
||||
}
|
||||
|
||||
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;
|
||||
#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>
|
||||
/// 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>
|
||||
#if UWP || WINUI
|
||||
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
|
||||
#else
|
||||
[System.ComponentModel.TypeConverter(typeof(TileSourceConverter))]
|
||||
#endif
|
||||
public class TileSource
|
||||
public virtual Uri GetUri(int zoomLevel, int column, int row)
|
||||
{
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a tile image without an Uri. Called when GetUri returns null.
|
||||
/// </summary>
|
||||
public virtual Task<ImageSource> LoadImageAsync(int zoomLevel, int column, int row)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
/// <summary>
|
||||
/// Loads a tile image without an Uri. Called when GetUri returns null.
|
||||
/// </summary>
|
||||
public virtual Task<ImageSource> LoadImageAsync(int zoomLevel, int column, int row)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public virtual Task<ImageSource> LoadImageAsync(Uri uri)
|
||||
{
|
||||
return ImageLoader.LoadImageAsync(uri);
|
||||
}
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public virtual Task<ImageSource> LoadImageAsync(Uri uri)
|
||||
{
|
||||
return ImageLoader.LoadImageAsync(uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a tile image from an encoded image buffer. Called when the
|
||||
/// TileImageLoader caches image buffers from http or https requests.
|
||||
/// </summary>
|
||||
public virtual Task<ImageSource> LoadImageAsync(byte[] buffer)
|
||||
{
|
||||
return ImageLoader.LoadImageAsync(buffer);
|
||||
}
|
||||
/// <summary>
|
||||
/// Loads a tile image from an encoded image buffer. Called when the
|
||||
/// TileImageLoader caches image buffers from http or https requests.
|
||||
/// </summary>
|
||||
public virtual Task<ImageSource> LoadImageAsync(byte[] buffer)
|
||||
{
|
||||
return ImageLoader.LoadImageAsync(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a TileSource instance from an Uri template string.
|
||||
/// </summary>
|
||||
public static TileSource Parse(string uriTemplate)
|
||||
{
|
||||
return new UriTileSource { UriTemplate = uriTemplate };
|
||||
}
|
||||
/// <summary>
|
||||
/// Creates a TileSource instance from an Uri template string.
|
||||
/// </summary>
|
||||
public static TileSource Parse(string uriTemplate)
|
||||
{
|
||||
return new UriTileSource { UriTemplate = uriTemplate };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,165 +6,164 @@ using System.Windows.Media;
|
|||
using Avalonia;
|
||||
#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>
|
||||
/// 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
|
||||
private readonly double n;
|
||||
private readonly double m; // 2*sqrt(n)/(1+n)
|
||||
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)
|
||||
{
|
||||
private readonly double n;
|
||||
private readonly double m; // 2*sqrt(n)/(1+n)
|
||||
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;
|
||||
EquatorialRadius = equatorialRadius;
|
||||
Flattening = flattening;
|
||||
|
||||
protected TransverseMercatorProjection(double equatorialRadius, double flattening)
|
||||
{
|
||||
EquatorialRadius = equatorialRadius;
|
||||
Flattening = flattening;
|
||||
n = flattening / (2d - flattening);
|
||||
m = 2d * Math.Sqrt(n) / (1d + n);
|
||||
var n2 = n * n;
|
||||
var n3 = n * n2;
|
||||
|
||||
n = flattening / (2d - flattening);
|
||||
m = 2d * Math.Sqrt(n) / (1d + n);
|
||||
var n2 = n * n;
|
||||
var n3 = n * n2;
|
||||
a1 = n / 2d - n2 * 2d / 3d + n3 * 5d / 16d;
|
||||
a2 = n2 * 13d / 48d - n3 * 3d / 5d;
|
||||
a3 = n3 * 61d / 240d;
|
||||
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;
|
||||
a2 = n2 * 13d / 48d - n3 * 3d / 5d;
|
||||
a3 = n3 * 61d / 240d;
|
||||
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);
|
||||
}
|
||||
public override double GridConvergence(double latitude, double longitude)
|
||||
{
|
||||
// φ
|
||||
var phi = latitude * Math.PI / 180d;
|
||||
// λ - λ0
|
||||
var lambda = (longitude - CentralMeridian) * Math.PI / 180d;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// γ 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 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 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);
|
||||
var k = ScaleFactor * A / EquatorialRadius
|
||||
* Math.Sqrt((1d + q * q) * (sigma * sigma + tau * tau) / (t * t + cosLambda * cosLambda));
|
||||
|
||||
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));
|
||||
// γ, grid convergence
|
||||
var gamma = Math.Atan2(tau * u + sigma * t * tanLambda, sigma * u - tau * t * tanLambda);
|
||||
|
||||
// γ, grid convergence
|
||||
var gamma = Math.Atan2(tau * u + sigma * t * tanLambda, sigma * u - tau * t * tanLambda);
|
||||
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
|
||||
transform.Rotate(-gamma * 180d / Math.PI);
|
||||
|
||||
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
|
||||
transform.Rotate(-gamma * 180d / Math.PI);
|
||||
return transform;
|
||||
}
|
||||
|
||||
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 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));
|
||||
var x = FalseEasting + ScaleFactor * A * (eta_ +
|
||||
a1 * Math.Cos(2d * xi_) * Math.Sinh(2d * eta_) +
|
||||
a2 * Math.Cos(4d * xi_) * Math.Sinh(4d * eta_) +
|
||||
a3 * Math.Cos(6d * xi_) * Math.Sinh(6d * eta_));
|
||||
|
||||
var x = FalseEasting + ScaleFactor * A * (eta_ +
|
||||
a1 * Math.Cos(2d * xi_) * Math.Sinh(2d * eta_) +
|
||||
a2 * Math.Cos(4d * xi_) * Math.Sinh(4d * eta_) +
|
||||
a3 * Math.Cos(6d * xi_) * Math.Sinh(6d * eta_));
|
||||
var y = FalseNorthing + ScaleFactor * A * (xi_ +
|
||||
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_));
|
||||
|
||||
var y = FalseNorthing + ScaleFactor * A * (xi_ +
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// ξ
|
||||
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);
|
||||
}
|
||||
return new Location(
|
||||
phi * 180d / Math.PI,
|
||||
lambda * 180d / Math.PI + CentralMeridian);
|
||||
}
|
||||
|
||||
#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
|
||||
private static double Atanh(double x) => Math.Atanh(x);
|
||||
private static double Atanh(double x) => Math.Atanh(x);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,160 +6,159 @@ using System.Windows.Media;
|
|||
using Avalonia;
|
||||
#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>
|
||||
/// 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)
|
||||
{
|
||||
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;
|
||||
// λ - λ0
|
||||
var sinPhi = Math.Sin(phi);
|
||||
var cosPhi = Math.Cos(phi);
|
||||
var tanPhi = sinPhi / cosPhi;
|
||||
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 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 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;
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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);
|
||||
gamma = Math.Atan(Math.Tan(lambda) * sinPhi) * 180d / Math.PI;
|
||||
}
|
||||
|
||||
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 e4 = e2 * e2;
|
||||
var e6 = e2 * e4;
|
||||
var sinPhi = Math.Sin(phi);
|
||||
var cosPhi = Math.Cos(phi);
|
||||
var tanPhi = sinPhi / cosPhi;
|
||||
|
||||
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)
|
||||
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 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;
|
||||
#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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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 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();
|
||||
}
|
||||
return Location.Parse(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 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();
|
||||
}
|
||||
return ConvertFrom(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 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();
|
||||
}
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class LocationCollectionConverter : 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 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 override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||
{
|
||||
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 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;
|
||||
#endif
|
||||
|
||||
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
|
||||
}
|
||||
namespace MapControl;
|
||||
|
||||
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
|
||||
element.IsVisible = visible;
|
||||
element.RenderTransformOrigin = center ? RelativePoint.Center : RelativePoint.TopLeft;
|
||||
#else
|
||||
element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
||||
#endif
|
||||
if (center)
|
||||
{
|
||||
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;
|
||||
#endif
|
||||
|
||||
namespace MapControl
|
||||
{
|
||||
internal class UpdateTimer : DispatcherTimer
|
||||
{
|
||||
public void Run(bool restart = false)
|
||||
{
|
||||
if (restart)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
namespace MapControl;
|
||||
|
||||
if (!IsEnabled)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
internal class UpdateTimer : DispatcherTimer
|
||||
{
|
||||
public void Run(bool restart = false)
|
||||
{
|
||||
if (restart)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
if (!IsEnabled)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +1,53 @@
|
|||
using System;
|
||||
|
||||
namespace MapControl
|
||||
namespace MapControl;
|
||||
|
||||
public class UriTileSource : TileSource
|
||||
{
|
||||
public class UriTileSource : TileSource
|
||||
private string uriFormat;
|
||||
|
||||
public string UriTemplate
|
||||
{
|
||||
private string uriFormat;
|
||||
|
||||
public string UriTemplate
|
||||
get;
|
||||
set
|
||||
{
|
||||
get;
|
||||
set
|
||||
field = value;
|
||||
uriFormat = field
|
||||
.Replace("{z}", "{0}")
|
||||
.Replace("{x}", "{1}")
|
||||
.Replace("{y}", "{2}")
|
||||
.Replace("{s}", "{3}");
|
||||
|
||||
if (Subdomains == null && field.Contains("{s}"))
|
||||
{
|
||||
field = value;
|
||||
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
|
||||
}
|
||||
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;
|
||||
|
||||
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)
|
||||
: base(equatorialRadius, flattening)
|
||||
{
|
||||
CrsId = crsId;
|
||||
ScaleFactor = 0.9996;
|
||||
CentralMeridian = zone * 6 - 183;
|
||||
FalseEasting = 5e5;
|
||||
FalseNorthing = north ? 0d : 1e7;
|
||||
Zone = zone;
|
||||
}
|
||||
|
||||
public int Zone { get; }
|
||||
CrsId = crsId;
|
||||
ScaleFactor = 0.9996;
|
||||
CentralMeridian = zone * 6 - 183;
|
||||
FalseEasting = 5e5;
|
||||
FalseNorthing = north ? 0d : 1e7;
|
||||
Zone = zone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WGS84 Universal Transverse Mercator Projection -
|
||||
/// EPSG:32601 to EPSG:32660 and EPSG:32701 to EPSG:32760.
|
||||
/// </summary>
|
||||
public class Wgs84UtmProjection : UtmProjection
|
||||
public int Zone { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
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)
|
||||
{
|
||||
if (zone < FirstZone || zone > LastZone)
|
||||
{
|
||||
throw new ArgumentException($"Invalid WGS84 UTM zone {zone}.", nameof(zone));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ETRS89 Universal Transverse Mercator Projection - EPSG:25828 to EPSG:25838.
|
||||
/// </summary>
|
||||
public class Etrs89UtmProjection : UtmProjection
|
||||
{
|
||||
public const int FirstZone = 28;
|
||||
public const int LastZone = 38;
|
||||
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
|
||||
{
|
||||
if (zone < FirstZone || zone > LastZone)
|
||||
{
|
||||
throw new ArgumentException($"Invalid ETRS89 UTM zone {zone}.", nameof(zone));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NAD83 Universal Transverse Mercator Projection - EPSG:26901 to EPSG:26923.
|
||||
/// </summary>
|
||||
public class Nad83UtmProjection : UtmProjection
|
||||
{
|
||||
public const int FirstZone = 1;
|
||||
public const int LastZone = 23;
|
||||
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
|
||||
{
|
||||
if (zone < FirstZone || zone > LastZone)
|
||||
{
|
||||
throw new ArgumentException($"Invalid NAD83 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>
|
||||
public class Etrs89UtmProjection : UtmProjection
|
||||
{
|
||||
public const int FirstZone = 28;
|
||||
public const int LastZone = 38;
|
||||
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
|
||||
{
|
||||
if (zone < FirstZone || zone > LastZone)
|
||||
{
|
||||
throw new ArgumentException($"Invalid ETRS89 UTM zone {zone}.", nameof(zone));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NAD83 Universal Transverse Mercator Projection - EPSG:26901 to EPSG:26923.
|
||||
/// </summary>
|
||||
public class Nad83UtmProjection : UtmProjection
|
||||
{
|
||||
public const int FirstZone = 1;
|
||||
public const int LastZone = 23;
|
||||
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
|
||||
{
|
||||
if (zone < FirstZone || zone > LastZone)
|
||||
{
|
||||
throw new ArgumentException($"Invalid NAD83 UTM zone {zone}.", nameof(zone));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,152 +6,151 @@ using System.Windows.Media;
|
|||
using Avalonia;
|
||||
#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>
|
||||
/// Defines the transformation between projected map coordinates in meters
|
||||
/// and view coordinates in pixels.
|
||||
/// Gets the scaling factor from projected map coordinates to view coordinates,
|
||||
/// as pixels per meter.
|
||||
/// </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>
|
||||
/// Gets the scaling factor from projected map coordinates to view coordinates,
|
||||
/// as pixels per meter.
|
||||
/// </summary>
|
||||
public double Scale { get; private set; }
|
||||
Scale = scale;
|
||||
Rotation = (rotation % 360d + 540d) % 360d - 180d;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rotation angle of the transform matrix.
|
||||
/// </summary>
|
||||
public double Rotation { get; private set; }
|
||||
var transform = new Matrix(scale, 0d, 0d, -scale, -scale * mapCenter.X, scale * mapCenter.Y);
|
||||
transform.Rotate(Rotation);
|
||||
transform.Translate(viewCenter.X, viewCenter.Y);
|
||||
MapToViewMatrix = transform;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transform matrix from projected map coordinates to view coordinates.
|
||||
/// </summary>
|
||||
public Matrix MapToViewMatrix { get; private set; }
|
||||
transform.Invert();
|
||||
ViewToMapMatrix = transform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transform matrix from view coordinates to projected map coordinates.
|
||||
/// </summary>
|
||||
public Matrix ViewToMapMatrix { get; private set; }
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a Point in projected map coordinates to a Point in view coordinates.
|
||||
/// </summary>
|
||||
public Point MapToView(Point point) => MapToViewMatrix.Transform(point);
|
||||
// Tile matrix origin in map coordinates.
|
||||
//
|
||||
var mapOrigin = new Point(
|
||||
tileMatrixTopLeft.X + tileMatrixOrigin.X / tileMatrixScale,
|
||||
tileMatrixTopLeft.Y - tileMatrixOrigin.Y / tileMatrixScale);
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a Point in view coordinates to a Point in projected map coordinates.
|
||||
/// </summary>
|
||||
public Point ViewToMap(Point point) => ViewToMapMatrix.Transform(point);
|
||||
// Tile matrix origin in view coordinates.
|
||||
//
|
||||
var viewOrigin = MapToViewMatrix.Transform(mapOrigin);
|
||||
transform.Translate(viewOrigin.X, viewOrigin.Y);
|
||||
|
||||
/// <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);
|
||||
return transform;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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)
|
||||
{
|
||||
Scale = scale;
|
||||
Rotation = (rotation % 360d + 540d) % 360d - 180d;
|
||||
x = x * transform.M11 + transform.OffsetX;
|
||||
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);
|
||||
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)
|
||||
if (width < 0d)
|
||||
{
|
||||
x = x * transform.M11 + transform.OffsetX;
|
||||
y = y * transform.M22 + transform.OffsetY;
|
||||
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;
|
||||
width = -width;
|
||||
x -= width;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
namespace MapControl;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
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>
|
||||
/// 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;
|
||||
#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>
|
||||
/// 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
|
||||
public const string DefaultCrsId = "EPSG:3857";
|
||||
|
||||
public WebMercatorProjection() // parameterless constructor for XAML
|
||||
: this(DefaultCrsId)
|
||||
{
|
||||
public const string DefaultCrsId = "EPSG:3857";
|
||||
}
|
||||
|
||||
public WebMercatorProjection() // parameterless constructor for XAML
|
||||
: this(DefaultCrsId)
|
||||
public WebMercatorProjection(string crsId)
|
||||
{
|
||||
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;
|
||||
CrsId = crsId;
|
||||
return double.PositiveInfinity;
|
||||
}
|
||||
|
||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||
{
|
||||
var k = 1d / Math.Cos(latitude * Math.PI / 180d); // p.44 (7-3)
|
||||
return Math.Log(Math.Tan((latitude + 90d) * Math.PI / 360d)) * 180d / Math.PI;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
#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>
|
||||
/// Displays a single map image from a Web Map Service (WMS).
|
||||
/// The base request URL.
|
||||
/// </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 =
|
||||
DependencyPropertyHelper.Register<WmsImageLayer, Uri>(nameof(ServiceUri), null,
|
||||
async (layer, oldValue, newValue) => await layer.UpdateImageAsync());
|
||||
/// <summary>
|
||||
/// Comma-separated sequence of requested WMS Styles. Default is an empty string.
|
||||
/// </summary>
|
||||
public string RequestStyles
|
||||
{
|
||||
get => (string)GetValue(RequestStylesProperty);
|
||||
set => SetValue(RequestStylesProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty RequestStylesProperty =
|
||||
DependencyPropertyHelper.Register<WmsImageLayer, string>(nameof(RequestStyles), "",
|
||||
async (layer, oldValue, newValue) => await layer.UpdateImageAsync());
|
||||
/// <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);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty RequestLayersProperty =
|
||||
DependencyPropertyHelper.Register<WmsImageLayer, string>(nameof(RequestLayers), null,
|
||||
async (layer, oldValue, newValue) => await layer.UpdateImageAsync());
|
||||
/// <summary>
|
||||
/// Gets a collection of all Layer names available in a WMS.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> AvailableLayers { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The base request URL.
|
||||
/// </summary>
|
||||
public Uri ServiceUri
|
||||
/// <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)
|
||||
{
|
||||
get => (Uri)GetValue(ServiceUriProperty);
|
||||
set => SetValue(ServiceUriProperty, value);
|
||||
}
|
||||
await InitializeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
if (AvailableLayers != null && AvailableLayers.Count > 0)
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
if (AvailableLayers != null && AvailableLayers.Count > 0)
|
||||
{
|
||||
await UpdateImageAsync();
|
||||
}
|
||||
await UpdateImageAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the AvailableLayers and SupportedCrsIds properties.
|
||||
/// Calling this method is only necessary when no layer name is known in advance.
|
||||
/// It is called internally in a Loaded event handler when the RequestLayers and AvailableLayers
|
||||
/// properties are null and the ServiceUri.Query part does not contain a LAYERS parameter.
|
||||
/// </summary>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var capabilities = await GetCapabilitiesAsync();
|
||||
|
||||
if (capabilities != null)
|
||||
{
|
||||
var ns = capabilities.Name.Namespace;
|
||||
var capability = capabilities.Element(ns + "Capability");
|
||||
|
||||
AvailableLayers = capability
|
||||
.Descendants(ns + "Layer")
|
||||
.Select(e => e.Element(ns + "Name")?.Value)
|
||||
.Where(n => !string.IsNullOrEmpty(n))
|
||||
.ToList();
|
||||
|
||||
SupportedCrsIds = capability
|
||||
.Descendants(ns + "Layer")
|
||||
.Descendants(ns + "CRS")
|
||||
.Select(e => e.Value)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an XElement from the URL returned by GetCapabilitiesRequestUri().
|
||||
/// </summary>
|
||||
public async Task<XElement> GetCapabilitiesAsync()
|
||||
{
|
||||
XElement element = null;
|
||||
|
||||
if (ServiceUri != null)
|
||||
{
|
||||
var uri = GetCapabilitiesRequestUri();
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = await ImageLoader.HttpClient.GetStreamAsync(uri);
|
||||
|
||||
element = await XDocument.LoadRootElementAsync(stream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "Failed reading capabilities from {uri}", uri);
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a response string from the URL returned by GetFeatureInfoRequestUri().
|
||||
/// </summary>
|
||||
public async Task<string> GetFeatureInfoAsync(Point position, string format = "text/plain")
|
||||
{
|
||||
string response = null;
|
||||
|
||||
if (ServiceUri != null && HasLayer &&
|
||||
ParentMap != null && ParentMap.InsideViewBounds(position) &&
|
||||
(SupportedCrsIds == null || SupportedCrsIds.Contains(ParentMap.MapProjection.CrsId)))
|
||||
{
|
||||
var uri = GetFeatureInfoRequestUri(position, format);
|
||||
|
||||
try
|
||||
{
|
||||
response = await ImageLoader.HttpClient.GetStringAsync(uri);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "Failed reading feature info from {uri}", uri);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an ImageSource from the URL returned by GetMapRequestUri().
|
||||
/// </summary>
|
||||
protected override async Task<ImageSource> GetImageAsync(Rect bbox, IProgress<double> progress)
|
||||
{
|
||||
ImageSource image = null;
|
||||
|
||||
if (ServiceUri != null && HasLayer &&
|
||||
(SupportedCrsIds == null || SupportedCrsIds.Contains(ParentMap.MapProjection.CrsId)))
|
||||
{
|
||||
var xMin = -180d * MapProjection.Wgs84MeterPerDegree;
|
||||
var xMax = 180d * MapProjection.Wgs84MeterPerDegree;
|
||||
|
||||
if (!ParentMap.MapProjection.IsNormalCylindrical ||
|
||||
bbox.X >= xMin && bbox.X + bbox.Width <= xMax)
|
||||
{
|
||||
var uri = GetMapRequestUri(bbox);
|
||||
|
||||
image = await ImageLoader.LoadImageAsync(uri, progress);
|
||||
}
|
||||
else
|
||||
{
|
||||
var x = bbox.X;
|
||||
|
||||
if (x < 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 bbox1 = new Rect(x, bbox.Y, width1, bbox.Height);
|
||||
var bbox2 = new Rect(xMin, bbox.Y, width2, bbox.Height);
|
||||
var uri1 = GetMapRequestUri(bbox1);
|
||||
var uri2 = GetMapRequestUri(bbox2);
|
||||
|
||||
image = await ImageLoader.LoadMergedImageAsync(uri1, uri2, progress);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a GetCapabilities request URL string.
|
||||
/// </summary>
|
||||
protected virtual Uri GetCapabilitiesRequestUri()
|
||||
{
|
||||
return GetRequestUri(new Dictionary<string, string>
|
||||
{
|
||||
{ "SERVICE", "WMS" },
|
||||
{ "VERSION", "1.3.0" },
|
||||
{ "REQUEST", "GetCapabilities" }
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a GetMap request URL string.
|
||||
/// </summary>
|
||||
protected virtual Uri GetMapRequestUri(Rect bbox)
|
||||
{
|
||||
var width = ParentMap.ViewTransform.Scale * bbox.Width;
|
||||
var height = ParentMap.ViewTransform.Scale * bbox.Height;
|
||||
|
||||
return GetRequestUri(new Dictionary<string, string>
|
||||
{
|
||||
{ "SERVICE", "WMS" },
|
||||
{ "VERSION", "1.3.0" },
|
||||
{ "REQUEST", "GetMap" },
|
||||
{ "LAYERS", RequestLayers ?? AvailableLayers?.FirstOrDefault() ?? "" },
|
||||
{ "STYLES", RequestStyles ?? "" },
|
||||
{ "FORMAT", "image/png" },
|
||||
{ "CRS", ParentMap.MapProjection.CrsId },
|
||||
{ "BBOX", GetBboxValue(bbox) },
|
||||
{ "WIDTH", Math.Ceiling(width).ToString("F0") },
|
||||
{ "HEIGHT", Math.Ceiling(height).ToString("F0") }
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a GetFeatureInfo request URL string.
|
||||
/// </summary>
|
||||
protected virtual Uri GetFeatureInfoRequestUri(Point position, string format)
|
||||
{
|
||||
var width = ParentMap.ActualWidth;
|
||||
var height = ParentMap.ActualHeight;
|
||||
var bbox = ParentMap.ViewTransform.ViewToMapBounds(new Rect(0d, 0d, width, height));
|
||||
|
||||
if (ParentMap.ViewTransform.Rotation != 0d)
|
||||
{
|
||||
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);
|
||||
transform.Translate(width / 2d, height / 2d);
|
||||
|
||||
position = transform.Transform(position);
|
||||
}
|
||||
|
||||
var queryParameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "SERVICE", "WMS" },
|
||||
{ "VERSION", "1.3.0" },
|
||||
{ "REQUEST", "GetFeatureInfo" },
|
||||
{ "LAYERS", RequestLayers ?? AvailableLayers?.FirstOrDefault() ?? "" },
|
||||
{ "STYLES", RequestStyles ?? "" },
|
||||
{ "INFO_FORMAT", format },
|
||||
{ "CRS", ParentMap.MapProjection.CrsId },
|
||||
{ "BBOX", GetBboxValue(bbox) },
|
||||
{ "WIDTH", Math.Ceiling(width).ToString("F0") },
|
||||
{ "HEIGHT", Math.Ceiling(height).ToString("F0") },
|
||||
{ "I", position.X.ToString("F0") },
|
||||
{ "J", position.Y.ToString("F0") }
|
||||
};
|
||||
|
||||
// GetRequestUri may modify queryParameters["LAYERS"].
|
||||
//
|
||||
var uriBuilder = new UriBuilder(GetRequestUri(queryParameters));
|
||||
|
||||
uriBuilder.Query += "&QUERY_LAYERS=" + queryParameters["LAYERS"];
|
||||
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
protected virtual Uri GetRequestUri(IDictionary<string, string> queryParameters)
|
||||
{
|
||||
var query = ServiceUri.Query;
|
||||
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
{
|
||||
// Parameters from ServiceUri.Query take higher precedence than queryParameters.
|
||||
//
|
||||
foreach (var param in query.Substring(1).Split('&'))
|
||||
{
|
||||
var pair = param.Split('=');
|
||||
queryParameters[pair[0]] = pair.Length > 1 ? pair[1] : "";
|
||||
}
|
||||
}
|
||||
|
||||
query = string.Join("&", queryParameters.Select(kv => kv.Key + "=" + kv.Value));
|
||||
|
||||
return new Uri(ServiceUri.GetLeftPart(UriPartial.Path) + "?" + query);
|
||||
}
|
||||
|
||||
protected virtual string GetBboxValue(Rect bbox)
|
||||
{
|
||||
var crs = ParentMap.MapProjection.CrsId;
|
||||
var format = "{0:0.###},{1:0.###},{2:0.###},{3:0.###}";
|
||||
var x1 = bbox.X;
|
||||
var y1 = bbox.Y;
|
||||
var x2 = bbox.X + bbox.Width;
|
||||
var y2 = bbox.Y + bbox.Height;
|
||||
|
||||
if (crs == "CRS:84" || crs == "EPSG:4326")
|
||||
{
|
||||
format = crs == "CRS:84"
|
||||
? "{0:0.########},{1:0.########},{2:0.########},{3:0.########}"
|
||||
: "{1:0.########},{0:0.########},{3:0.########},{2:0.########}";
|
||||
x1 /= MapProjection.Wgs84MeterPerDegree;
|
||||
y1 /= MapProjection.Wgs84MeterPerDegree;
|
||||
x2 /= MapProjection.Wgs84MeterPerDegree;
|
||||
y2 /= MapProjection.Wgs84MeterPerDegree;
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, format, x1, y1, x2, y2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the AvailableLayers and SupportedCrsIds properties.
|
||||
/// Calling this method is only necessary when no layer name is known in advance.
|
||||
/// It is called internally in a Loaded event handler when the RequestLayers and AvailableLayers
|
||||
/// properties are null and the ServiceUri.Query part does not contain a LAYERS parameter.
|
||||
/// </summary>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var capabilities = await GetCapabilitiesAsync();
|
||||
|
||||
if (capabilities != null)
|
||||
{
|
||||
var ns = capabilities.Name.Namespace;
|
||||
var capability = capabilities.Element(ns + "Capability");
|
||||
|
||||
AvailableLayers = capability
|
||||
.Descendants(ns + "Layer")
|
||||
.Select(e => e.Element(ns + "Name")?.Value)
|
||||
.Where(n => !string.IsNullOrEmpty(n))
|
||||
.ToList();
|
||||
|
||||
SupportedCrsIds = capability
|
||||
.Descendants(ns + "Layer")
|
||||
.Descendants(ns + "CRS")
|
||||
.Select(e => e.Value)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an XElement from the URL returned by GetCapabilitiesRequestUri().
|
||||
/// </summary>
|
||||
public async Task<XElement> GetCapabilitiesAsync()
|
||||
{
|
||||
XElement element = null;
|
||||
|
||||
if (ServiceUri != null)
|
||||
{
|
||||
var uri = GetCapabilitiesRequestUri();
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = await ImageLoader.HttpClient.GetStreamAsync(uri);
|
||||
|
||||
element = await XDocument.LoadRootElementAsync(stream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "Failed reading capabilities from {uri}", uri);
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a response string from the URL returned by GetFeatureInfoRequestUri().
|
||||
/// </summary>
|
||||
public async Task<string> GetFeatureInfoAsync(Point position, string format = "text/plain")
|
||||
{
|
||||
string response = null;
|
||||
|
||||
if (ServiceUri != null && HasLayer &&
|
||||
ParentMap != null && ParentMap.InsideViewBounds(position) &&
|
||||
(SupportedCrsIds == null || SupportedCrsIds.Contains(ParentMap.MapProjection.CrsId)))
|
||||
{
|
||||
var uri = GetFeatureInfoRequestUri(position, format);
|
||||
|
||||
try
|
||||
{
|
||||
response = await ImageLoader.HttpClient.GetStringAsync(uri);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "Failed reading feature info from {uri}", uri);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an ImageSource from the URL returned by GetMapRequestUri().
|
||||
/// </summary>
|
||||
protected override async Task<ImageSource> GetImageAsync(Rect bbox, IProgress<double> progress)
|
||||
{
|
||||
ImageSource image = null;
|
||||
|
||||
if (ServiceUri != null && HasLayer &&
|
||||
(SupportedCrsIds == null || SupportedCrsIds.Contains(ParentMap.MapProjection.CrsId)))
|
||||
{
|
||||
var xMin = -180d * MapProjection.Wgs84MeterPerDegree;
|
||||
var xMax = 180d * MapProjection.Wgs84MeterPerDegree;
|
||||
|
||||
if (!ParentMap.MapProjection.IsNormalCylindrical ||
|
||||
bbox.X >= xMin && bbox.X + bbox.Width <= xMax)
|
||||
{
|
||||
var uri = GetMapRequestUri(bbox);
|
||||
|
||||
image = await ImageLoader.LoadImageAsync(uri, progress);
|
||||
}
|
||||
else
|
||||
{
|
||||
var x = bbox.X;
|
||||
|
||||
if (x < 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 bbox1 = new Rect(x, bbox.Y, width1, bbox.Height);
|
||||
var bbox2 = new Rect(xMin, bbox.Y, width2, bbox.Height);
|
||||
var uri1 = GetMapRequestUri(bbox1);
|
||||
var uri2 = GetMapRequestUri(bbox2);
|
||||
|
||||
image = await ImageLoader.LoadMergedImageAsync(uri1, uri2, progress);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a GetCapabilities request URL string.
|
||||
/// </summary>
|
||||
protected virtual Uri GetCapabilitiesRequestUri()
|
||||
{
|
||||
return GetRequestUri(new Dictionary<string, string>
|
||||
{
|
||||
{ "SERVICE", "WMS" },
|
||||
{ "VERSION", "1.3.0" },
|
||||
{ "REQUEST", "GetCapabilities" }
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a GetMap request URL string.
|
||||
/// </summary>
|
||||
protected virtual Uri GetMapRequestUri(Rect bbox)
|
||||
{
|
||||
var width = ParentMap.ViewTransform.Scale * bbox.Width;
|
||||
var height = ParentMap.ViewTransform.Scale * bbox.Height;
|
||||
|
||||
return GetRequestUri(new Dictionary<string, string>
|
||||
{
|
||||
{ "SERVICE", "WMS" },
|
||||
{ "VERSION", "1.3.0" },
|
||||
{ "REQUEST", "GetMap" },
|
||||
{ "LAYERS", RequestLayers ?? AvailableLayers?.FirstOrDefault() ?? "" },
|
||||
{ "STYLES", RequestStyles ?? "" },
|
||||
{ "FORMAT", "image/png" },
|
||||
{ "CRS", ParentMap.MapProjection.CrsId },
|
||||
{ "BBOX", GetBboxValue(bbox) },
|
||||
{ "WIDTH", Math.Ceiling(width).ToString("F0") },
|
||||
{ "HEIGHT", Math.Ceiling(height).ToString("F0") }
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a GetFeatureInfo request URL string.
|
||||
/// </summary>
|
||||
protected virtual Uri GetFeatureInfoRequestUri(Point position, string format)
|
||||
{
|
||||
var width = ParentMap.ActualWidth;
|
||||
var height = ParentMap.ActualHeight;
|
||||
var bbox = ParentMap.ViewTransform.ViewToMapBounds(new Rect(0d, 0d, width, height));
|
||||
|
||||
if (ParentMap.ViewTransform.Rotation != 0d)
|
||||
{
|
||||
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);
|
||||
transform.Translate(width / 2d, height / 2d);
|
||||
|
||||
position = transform.Transform(position);
|
||||
}
|
||||
|
||||
var queryParameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "SERVICE", "WMS" },
|
||||
{ "VERSION", "1.3.0" },
|
||||
{ "REQUEST", "GetFeatureInfo" },
|
||||
{ "LAYERS", RequestLayers ?? AvailableLayers?.FirstOrDefault() ?? "" },
|
||||
{ "STYLES", RequestStyles ?? "" },
|
||||
{ "INFO_FORMAT", format },
|
||||
{ "CRS", ParentMap.MapProjection.CrsId },
|
||||
{ "BBOX", GetBboxValue(bbox) },
|
||||
{ "WIDTH", Math.Ceiling(width).ToString("F0") },
|
||||
{ "HEIGHT", Math.Ceiling(height).ToString("F0") },
|
||||
{ "I", position.X.ToString("F0") },
|
||||
{ "J", position.Y.ToString("F0") }
|
||||
};
|
||||
|
||||
// GetRequestUri may modify queryParameters["LAYERS"].
|
||||
//
|
||||
var uriBuilder = new UriBuilder(GetRequestUri(queryParameters));
|
||||
|
||||
uriBuilder.Query += "&QUERY_LAYERS=" + queryParameters["LAYERS"];
|
||||
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
protected virtual Uri GetRequestUri(IDictionary<string, string> queryParameters)
|
||||
{
|
||||
var query = ServiceUri.Query;
|
||||
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
{
|
||||
// Parameters from ServiceUri.Query take higher precedence than queryParameters.
|
||||
//
|
||||
foreach (var param in query.Substring(1).Split('&'))
|
||||
{
|
||||
var pair = param.Split('=');
|
||||
queryParameters[pair[0]] = pair.Length > 1 ? pair[1] : "";
|
||||
}
|
||||
}
|
||||
|
||||
query = string.Join("&", queryParameters.Select(kv => kv.Key + "=" + kv.Value));
|
||||
|
||||
return new Uri(ServiceUri.GetLeftPart(UriPartial.Path) + "?" + query);
|
||||
}
|
||||
|
||||
protected virtual string GetBboxValue(Rect bbox)
|
||||
{
|
||||
var crs = ParentMap.MapProjection.CrsId;
|
||||
var format = "{0:0.###},{1:0.###},{2:0.###},{3:0.###}";
|
||||
var x1 = bbox.X;
|
||||
var y1 = bbox.Y;
|
||||
var x2 = bbox.X + bbox.Width;
|
||||
var y2 = bbox.Y + bbox.Height;
|
||||
|
||||
if (crs == "CRS:84" || crs == "EPSG:4326")
|
||||
{
|
||||
format = crs == "CRS:84"
|
||||
? "{0:0.########},{1:0.########},{2:0.########},{3:0.########}"
|
||||
: "{1:0.########},{0:0.########},{3:0.########},{2:0.########}";
|
||||
x1 /= MapProjection.Wgs84MeterPerDegree;
|
||||
y1 /= MapProjection.Wgs84MeterPerDegree;
|
||||
x2 /= MapProjection.Wgs84MeterPerDegree;
|
||||
y2 /= MapProjection.Wgs84MeterPerDegree;
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, format, x1, y1, x2, y2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,293 +11,292 @@ using System.Windows;
|
|||
using Avalonia;
|
||||
#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>
|
||||
/// See https://www.ogc.org/standards/wmts, 07-057r7_Web_Map_Tile_Service_Standard.pdf.
|
||||
/// </summary>
|
||||
public class WmtsCapabilities
|
||||
private static readonly XNamespace ows = "http://www.opengis.net/ows/1.1";
|
||||
private static readonly XNamespace wmts = "http://www.opengis.net/wmts/1.0";
|
||||
private static readonly XNamespace xlink = "http://www.w3.org/1999/xlink";
|
||||
|
||||
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";
|
||||
private static readonly XNamespace wmts = "http://www.opengis.net/wmts/1.0";
|
||||
private static readonly XNamespace xlink = "http://www.w3.org/1999/xlink";
|
||||
Stream xmlStream;
|
||||
string defaultUri = null;
|
||||
|
||||
public string Layer { get; private set; }
|
||||
public List<WmtsTileMatrixSet> TileMatrixSets { get; private set; }
|
||||
|
||||
public static async Task<WmtsCapabilities> ReadCapabilitiesAsync(Uri uri, string layer)
|
||||
if (!uri.IsAbsoluteUri)
|
||||
{
|
||||
Stream xmlStream;
|
||||
string defaultUri = null;
|
||||
xmlStream = File.OpenRead(uri.OriginalString);
|
||||
}
|
||||
else if (uri.IsFile)
|
||||
{
|
||||
xmlStream = File.OpenRead(uri.LocalPath);
|
||||
}
|
||||
else if (uri.IsHttp())
|
||||
{
|
||||
defaultUri = uri.OriginalString.Split('?')[0];
|
||||
|
||||
if (!uri.IsAbsoluteUri)
|
||||
{
|
||||
xmlStream = File.OpenRead(uri.OriginalString);
|
||||
}
|
||||
else if (uri.IsFile)
|
||||
{
|
||||
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);
|
||||
xmlStream = await ImageLoader.HttpClient.GetStreamAsync(uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Invalid Capabilities Uri: {uri}");
|
||||
}
|
||||
|
||||
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") ??
|
||||
throw new ArgumentException("Contents element not found.");
|
||||
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.");
|
||||
|
||||
XElement layerElement;
|
||||
|
||||
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
|
||||
};
|
||||
layer = layerElement.Element(ows + "Identifier")?.Value ?? "";
|
||||
}
|
||||
|
||||
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";
|
||||
const string formatJpg = "image/jpeg";
|
||||
string uriTemplate = null;
|
||||
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}\".");
|
||||
|
||||
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);
|
||||
tileMatrixSets.Add(ReadTileMatrixSet(tileMatrixSetElement, uriTemplate));
|
||||
}
|
||||
|
||||
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]
|
||||
: resourceUrls.Contains(formatJpg) ? resourceUrls[formatJpg]
|
||||
: resourceUrls.First();
|
||||
var formats = layerElement
|
||||
.Elements(wmts + "Format")
|
||||
.Select(f => f.Value);
|
||||
|
||||
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;
|
||||
var format = formats.Contains(formatPng) ? formatPng
|
||||
: formats.Contains(formatJpg) ? formatJpg
|
||||
: formats.FirstOrDefault();
|
||||
|
||||
if (uriTemplate != null)
|
||||
if (string.IsNullOrEmpty(format))
|
||||
{
|
||||
var formats = layerElement
|
||||
.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}";
|
||||
format = formatPng;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(uriTemplate))
|
||||
{
|
||||
throw new ArgumentException($"No ResourceURL element in Layer \"{layer}\" and no GetTile KVP Operation Metadata found.");
|
||||
uriTemplate +=
|
||||
"?Service=WMTS" +
|
||||
"&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;
|
||||
|
||||
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);
|
||||
throw new ArgumentException($"No ResourceURL element in Layer \"{layer}\" and no GetTile KVP Operation Metadata found.");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
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)
|
||||
{
|
||||
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;
|
||||
#endif
|
||||
|
||||
namespace MapControl
|
||||
{
|
||||
namespace MapControl;
|
||||
|
||||
#if WPF
|
||||
using TileMatrixLayer = DrawingTileMatrixLayer;
|
||||
using TileMatrixLayer = DrawingTileMatrixLayer;
|
||||
#else
|
||||
using TileMatrixLayer = WmtsTileMatrixLayer;
|
||||
using TileMatrixLayer = WmtsTileMatrixLayer;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Displays map tiles from a Web Map Tile Service (WMTS).
|
||||
/// </summary>
|
||||
public partial class WmtsTileLayer : TilePyramidLayer
|
||||
/// <summary>
|
||||
/// Displays map tiles from a Web Map Tile Service (WMTS).
|
||||
/// </summary>
|
||||
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 =
|
||||
DependencyPropertyHelper.Register<WmtsTileLayer, Uri>(nameof(CapabilitiesUri));
|
||||
/// <summary>
|
||||
/// 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 =
|
||||
DependencyPropertyHelper.Register<WmtsTileLayer, string>(nameof(Layer));
|
||||
/// <summary>
|
||||
/// 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 =
|
||||
DependencyPropertyHelper.Register<WmtsTileLayer, string[]>(nameof(PreferredTileMatrixSets));
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// The Uri of a XML file or web response that contains the service capabilities.
|
||||
/// </summary>
|
||||
public Uri CapabilitiesUri
|
||||
return new Size();
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
foreach (var layer in ChildLayers)
|
||||
{
|
||||
get => (Uri)GetValue(CapabilitiesUriProperty);
|
||||
set => SetValue(CapabilitiesUriProperty, value);
|
||||
layer.Arrange(new Rect(0d, 0d, finalSize.Width, finalSize.Height));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
protected override void UpdateRenderTransform()
|
||||
{
|
||||
foreach (var layer in ChildLayers)
|
||||
{
|
||||
get => (string)GetValue(LayerProperty);
|
||||
set => SetValue(LayerProperty, value);
|
||||
layer.UpdateRenderTransform(ParentMap.ViewTransform);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In case there are TileMatrixSets with identical SupportedCRS values,
|
||||
/// the ones with Identifiers contained in this collection take precedence.
|
||||
/// </summary>
|
||||
public string[] PreferredTileMatrixSets
|
||||
protected override void UpdateTileCollection()
|
||||
{
|
||||
if (ParentMap == null ||
|
||||
!TileMatrixSets.TryGetValue(ParentMap.MapProjection.CrsId, out WmtsTileMatrixSet tileMatrixSet))
|
||||
{
|
||||
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();
|
||||
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.
|
||||
//
|
||||
var layer = layers.FirstOrDefault(layer => layer.WmtsTileMatrix == tileMatrix) ??
|
||||
new TileMatrixLayer(tileMatrix, tileMatrixSet.IndexOf(tileMatrix));
|
||||
|
||||
if (layer.UpdateTiles(ParentMap.ViewTransform, ParentMap.ActualWidth, ParentMap.ActualHeight))
|
||||
if (!string.IsNullOrEmpty(Layer))
|
||||
{
|
||||
tilesChanged = true;
|
||||
cacheName += "/" + Layer.Replace(':', '_');
|
||||
}
|
||||
|
||||
layer.UpdateRenderTransform(ParentMap.ViewTransform);
|
||||
|
||||
Children.Add(layer);
|
||||
if (!string.IsNullOrEmpty(tileMatrixSet.Identifier))
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
TileMatrixSets[tms.SupportedCrsId] = tms;
|
||||
}
|
||||
|
||||
UpdateTileCollection();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "Failed reading capabilities from {uri}", CapabilitiesUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,22 +5,21 @@ using System.Windows;
|
|||
using Avalonia;
|
||||
#endif
|
||||
|
||||
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;
|
||||
namespace MapControl;
|
||||
|
||||
// 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;
|
||||
}
|
||||
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 bool HasFullHorizontalCoverage { get; } =
|
||||
Math.Abs(matrixWidth * tileWidth / scale - 360d * MapProjection.Wgs84MeterPerDegree) < 1e-3;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,107 +18,106 @@ using Avalonia.Controls;
|
|||
using Avalonia.Media;
|
||||
#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());
|
||||
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.
|
||||
// Set X range limits.
|
||||
//
|
||||
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
|
||||
|
||||
((MatrixTransform)RenderTransform).Matrix =
|
||||
viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
|
||||
xMin = Math.Max(xMin, 0);
|
||||
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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;
|
||||
return false;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
tile.Image.Measure(availableSize);
|
||||
}
|
||||
|
||||
return new Size();
|
||||
Children.Add(tile.Image);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
foreach (var tile in Tiles)
|
||||
{
|
||||
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;
|
||||
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 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;
|
||||
#endif
|
||||
|
||||
namespace MapControl
|
||||
namespace MapControl;
|
||||
|
||||
public class WmtsTileMatrixSet(
|
||||
string identifier, string supportedCrsId, string uriTemplate, IEnumerable<WmtsTileMatrix> tileMatrixes)
|
||||
{
|
||||
public class WmtsTileMatrixSet(
|
||||
string identifier, string supportedCrsId, string uriTemplate, IEnumerable<WmtsTileMatrix> tileMatrixes)
|
||||
public string Identifier => identifier;
|
||||
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;
|
||||
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)
|
||||
{
|
||||
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 scale = matrixSize * tileSize / (2d * origin);
|
||||
var matrixSize = 1 << zoomLevel;
|
||||
var scale = matrixSize * tileSize / (2d * origin);
|
||||
|
||||
return new WmtsTileMatrix(
|
||||
zoomLevel.ToString(), scale, new Point(-origin, origin),
|
||||
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 WmtsTileMatrix(
|
||||
zoomLevel.ToString(), scale, new Point(-origin, origin),
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
using System;
|
||||
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
|
||||
.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)
|
||||
{
|
||||
return zoomLevel < tileMatrixes.Count
|
||||
? new Uri(string.Format(uriFormat, tileMatrixes[zoomLevel].Identifier, column, row))
|
||||
: null;
|
||||
}
|
||||
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;
|
||||
#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>
|
||||
/// 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
|
||||
public const string DefaultCrsId = "EPSG:3395";
|
||||
|
||||
public WorldMercatorProjection() // parameterless constructor for XAML
|
||||
: this(DefaultCrsId)
|
||||
{
|
||||
public const string DefaultCrsId = "EPSG:3395";
|
||||
}
|
||||
|
||||
public WorldMercatorProjection() // parameterless constructor for XAML
|
||||
: this(DefaultCrsId)
|
||||
public WorldMercatorProjection(string crsId)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public WorldMercatorProjection(string crsId)
|
||||
else if (latitude >= 90d)
|
||||
{
|
||||
IsNormalCylindrical = true;
|
||||
CrsId = crsId;
|
||||
y = double.PositiveInfinity;
|
||||
}
|
||||
|
||||
public override Matrix RelativeTransform(double latitude, double longitude)
|
||||
else
|
||||
{
|
||||
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)
|
||||
var e = Math.Sqrt((2d - Flattening) * Flattening);
|
||||
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)
|
||||
{
|
||||
var x = EquatorialRadius * longitude * Math.PI / 180d;
|
||||
double y;
|
||||
return new Point(x, y);
|
||||
}
|
||||
|
||||
if (latitude <= -90d)
|
||||
{
|
||||
y = double.NegativeInfinity;
|
||||
}
|
||||
else if (latitude >= 90d)
|
||||
{
|
||||
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);
|
||||
public override Location MapToLocation(double x, double y)
|
||||
{
|
||||
var t = Math.Exp(-y / EquatorialRadius); // p.44 (7-10)
|
||||
var phi = ApproximateLatitude((2d - Flattening) * Flattening, t); // p.45 (3-5)
|
||||
var lambda = x / EquatorialRadius;
|
||||
|
||||
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)
|
||||
{
|
||||
var t = Math.Exp(-y / EquatorialRadius); // p.44 (7-10)
|
||||
var phi = ApproximateLatitude((2d - Flattening) * Flattening, t); // p.45 (3-5)
|
||||
var lambda = x / EquatorialRadius;
|
||||
|
||||
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)
|
||||
}
|
||||
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.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
|
||||
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
|
||||
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
|
||||
return document.Root;
|
||||
}
|
||||
return document.Root;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue