File scoped namespaces

This commit is contained in:
ClemensFischer 2026-04-13 17:14:49 +02:00
parent c14377f976
commit 65aba44af6
152 changed files with 11962 additions and 12115 deletions

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -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
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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
{
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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
};
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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
});
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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);
}
}
}

View file

@ -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 };
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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();
}
}

View file

@ -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
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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)
}
}

View file

@ -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;
}
}