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

@ -6,98 +6,97 @@ using System;
#pragma warning disable AVP1001
namespace MapControl
namespace MapControl;
public static class DependencyPropertyHelper
{
public static class DependencyPropertyHelper
public static AttachedProperty<TValue> RegisterAttached<TValue>(
string name,
Type ownerType,
TValue defaultValue = default,
Action<Control, TValue, TValue> changed = null,
bool inherits = false)
{
public static AttachedProperty<TValue> RegisterAttached<TValue>(
string name,
Type ownerType,
TValue defaultValue = default,
Action<Control, TValue, TValue> changed = null,
bool inherits = false)
var property = AvaloniaProperty.RegisterAttached<Control, TValue>(name, ownerType, defaultValue, inherits);
if (changed != null)
{
var property = AvaloniaProperty.RegisterAttached<Control, TValue>(name, ownerType, defaultValue, inherits);
if (changed != null)
{
property.Changed.AddClassHandler<Control, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
}
return property;
property.Changed.AddClassHandler<Control, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
}
public static StyledProperty<TValue> Register<TOwner, TValue>(
string name,
TValue defaultValue = default,
Action<TOwner, TValue, TValue> changed = null,
Func<TOwner, TValue, TValue> coerce = null,
bool bindTwoWayByDefault = false)
where TOwner : AvaloniaObject
return property;
}
public static StyledProperty<TValue> Register<TOwner, TValue>(
string name,
TValue defaultValue = default,
Action<TOwner, TValue, TValue> changed = null,
Func<TOwner, TValue, TValue> coerce = null,
bool bindTwoWayByDefault = false)
where TOwner : AvaloniaObject
{
Func<AvaloniaObject, TValue, TValue> coerceFunc = null;
if (coerce != null)
{
Func<AvaloniaObject, TValue, TValue> coerceFunc = null;
if (coerce != null)
{
// Do not coerce default value.
//
coerceFunc = (obj, value) => Equals(value, defaultValue) ? value : coerce((TOwner)obj, value);
}
var bindingMode = bindTwoWayByDefault ? BindingMode.TwoWay : BindingMode.OneWay;
var property = AvaloniaProperty.Register<TOwner, TValue>(name, defaultValue, false, bindingMode, null, coerceFunc);
if (changed != null)
{
property.Changed.AddClassHandler<TOwner, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
}
return property;
// Do not coerce default value.
//
coerceFunc = (obj, value) => Equals(value, defaultValue) ? value : coerce((TOwner)obj, value);
}
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
StyledProperty<TValue> source) where TOwner : AvaloniaObject
{
return source.AddOwner<TOwner>();
}
var bindingMode = bindTwoWayByDefault ? BindingMode.TwoWay : BindingMode.OneWay;
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
StyledProperty<TValue> source,
TValue defaultValue) where TOwner : AvaloniaObject
{
return source.AddOwner<TOwner>(new StyledPropertyMetadata<TValue>(new Optional<TValue>(defaultValue)));
}
var property = AvaloniaProperty.Register<TOwner, TValue>(name, defaultValue, false, bindingMode, null, coerceFunc);
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
StyledProperty<TValue> source,
Action<TOwner, TValue, TValue> changed) where TOwner : AvaloniaObject
if (changed != null)
{
var property = source.AddOwner<TOwner>();
property.Changed.AddClassHandler<TOwner, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
return property;
}
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
StyledProperty<TValue> source) where TOwner : AvaloniaObject
{
return AddOwner<TOwner, TValue>(source);
}
return property;
}
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
StyledProperty<TValue> source,
Action<TOwner, TValue, TValue> changed) where TOwner : AvaloniaObject
{
return AddOwner(source, changed);
}
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
StyledProperty<TValue> source) where TOwner : AvaloniaObject
{
return source.AddOwner<TOwner>();
}
public static void SetBinding(this AvaloniaObject target, AvaloniaProperty property, Binding binding)
{
target.Bind(property, binding);
}
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
StyledProperty<TValue> source,
TValue defaultValue) where TOwner : AvaloniaObject
{
return source.AddOwner<TOwner>(new StyledPropertyMetadata<TValue>(new Optional<TValue>(defaultValue)));
}
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
StyledProperty<TValue> source,
Action<TOwner, TValue, TValue> changed) where TOwner : AvaloniaObject
{
var property = source.AddOwner<TOwner>();
property.Changed.AddClassHandler<TOwner, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
return property;
}
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
StyledProperty<TValue> source) where TOwner : AvaloniaObject
{
return AddOwner<TOwner, TValue>(source);
}
public static StyledProperty<TValue> AddOwner<TOwner, TValue>(
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
StyledProperty<TValue> source,
Action<TOwner, TValue, TValue> changed) where TOwner : AvaloniaObject
{
return AddOwner(source, changed);
}
public static void SetBinding(this AvaloniaObject target, AvaloniaProperty property, Binding binding)
{
target.Bind(property, binding);
}
}

View file

@ -1,13 +1,12 @@
using System;
using System.Threading.Tasks;
namespace MapControl
namespace MapControl;
public static partial class GeoImage
{
public static partial class GeoImage
private static Task<GeoBitmap> LoadGeoTiff(string sourcePath)
{
private static Task<GeoBitmap> LoadGeoTiff(string sourcePath)
{
throw new InvalidOperationException("GeoTIFF is not supported.");
}
throw new InvalidOperationException("GeoTIFF is not supported.");
}
}

View file

@ -7,79 +7,78 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MapControl
namespace MapControl;
public static partial class ImageLoader
{
public static partial class ImageLoader
public static IImage LoadResourceImage(Uri uri)
{
public static IImage LoadResourceImage(Uri uri)
return new Bitmap(AssetLoader.Open(uri));
}
public static IImage LoadImage(Stream stream)
{
return new Bitmap(stream);
}
public static IImage LoadImage(string path)
{
return File.Exists(path) ? new Bitmap(path) : null;
}
public static Task<IImage> LoadImageAsync(Stream stream)
{
return Thread.CurrentThread.IsThreadPoolThread ?
Task.FromResult(LoadImage(stream)) :
Task.Run(() => LoadImage(stream));
}
public static Task<IImage> LoadImageAsync(string path)
{
return Thread.CurrentThread.IsThreadPoolThread ?
Task.FromResult(LoadImage(path)) :
Task.Run(() => LoadImage(path));
}
internal static async Task<IImage> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
{
Bitmap mergedBitmap = null;
var p1 = 0d;
var p2 = 0d;
var images = await Task.WhenAll(
LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
if (images.Length == 2 &&
images[0] is Bitmap bitmap1 &&
images[1] is Bitmap bitmap2 &&
bitmap1.PixelSize.Height == bitmap2.PixelSize.Height &&
bitmap1.Format.HasValue &&
bitmap1.Format == bitmap2.Format &&
bitmap1.AlphaFormat.HasValue &&
bitmap1.AlphaFormat == bitmap2.AlphaFormat)
{
return new Bitmap(AssetLoader.Open(uri));
}
var bpp = bitmap1.Format.Value == PixelFormat.Rgb565 ? 2 : 4;
var pixelSize = new PixelSize(bitmap1.PixelSize.Width + bitmap2.PixelSize.Width, bitmap1.PixelSize.Height);
var stride1 = bpp * bitmap1.PixelSize.Width;
var stride = bpp * pixelSize.Width;
var bufferSize = stride * pixelSize.Height;
public static IImage LoadImage(Stream stream)
{
return new Bitmap(stream);
}
public static IImage LoadImage(string path)
{
return File.Exists(path) ? new Bitmap(path) : null;
}
public static Task<IImage> LoadImageAsync(Stream stream)
{
return Thread.CurrentThread.IsThreadPoolThread ?
Task.FromResult(LoadImage(stream)) :
Task.Run(() => LoadImage(stream));
}
public static Task<IImage> LoadImageAsync(string path)
{
return Thread.CurrentThread.IsThreadPoolThread ?
Task.FromResult(LoadImage(path)) :
Task.Run(() => LoadImage(path));
}
internal static async Task<IImage> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
{
Bitmap mergedBitmap = null;
var p1 = 0d;
var p2 = 0d;
var images = await Task.WhenAll(
LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
if (images.Length == 2 &&
images[0] is Bitmap bitmap1 &&
images[1] is Bitmap bitmap2 &&
bitmap1.PixelSize.Height == bitmap2.PixelSize.Height &&
bitmap1.Format.HasValue &&
bitmap1.Format == bitmap2.Format &&
bitmap1.AlphaFormat.HasValue &&
bitmap1.AlphaFormat == bitmap2.AlphaFormat)
unsafe
{
var bpp = bitmap1.Format.Value == PixelFormat.Rgb565 ? 2 : 4;
var pixelSize = new PixelSize(bitmap1.PixelSize.Width + bitmap2.PixelSize.Width, bitmap1.PixelSize.Height);
var stride1 = bpp * bitmap1.PixelSize.Width;
var stride = bpp * pixelSize.Width;
var bufferSize = stride * pixelSize.Height;
unsafe
fixed (byte* ptr = new byte[stride * pixelSize.Height])
{
fixed (byte* ptr = new byte[stride * pixelSize.Height])
{
var buffer = (nint)ptr;
var buffer = (nint)ptr;
bitmap1.CopyPixels(new PixelRect(bitmap1.PixelSize), buffer, bufferSize, stride);
bitmap2.CopyPixels(new PixelRect(bitmap2.PixelSize), buffer + stride1, bufferSize, stride);
bitmap1.CopyPixels(new PixelRect(bitmap1.PixelSize), buffer, bufferSize, stride);
bitmap2.CopyPixels(new PixelRect(bitmap2.PixelSize), buffer + stride1, bufferSize, stride);
mergedBitmap = new Bitmap(bitmap1.Format.Value, bitmap1.AlphaFormat.Value, buffer, pixelSize, bitmap1.Dpi, stride);
}
mergedBitmap = new Bitmap(bitmap1.Format.Value, bitmap1.AlphaFormat.Value, buffer, pixelSize, bitmap1.Dpi, stride);
}
}
return mergedBitmap;
}
return mergedBitmap;
}
}

View file

@ -6,46 +6,45 @@ using Avalonia.Styling;
using System;
using System.Threading.Tasks;
namespace MapControl
namespace MapControl;
public class ImageTile(int zoomLevel, int x, int y, int columnCount)
: Tile(zoomLevel, x, y, columnCount)
{
public class ImageTile(int zoomLevel, int x, int y, int columnCount)
: Tile(zoomLevel, x, y, columnCount)
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
public override async Task LoadImageAsync(Func<Task<IImage>> loadImageFunc)
{
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
var image = await loadImageFunc().ConfigureAwait(false);
public override async Task LoadImageAsync(Func<Task<IImage>> loadImageFunc)
void SetImageSource()
{
var image = await loadImageFunc().ConfigureAwait(false);
Image.Source = image;
void SetImageSource()
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
{
Image.Source = image;
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
var fadeInAnimation = new Animation
{
var fadeInAnimation = new Animation
{
Duration = MapBase.ImageFadeDuration,
Children =
Duration = MapBase.ImageFadeDuration,
Children =
{
new KeyFrame
{
new KeyFrame
{
KeyTime = TimeSpan.Zero,
Setters = { new Setter(Visual.OpacityProperty, 0d) }
},
new KeyFrame
{
KeyTime = MapBase.ImageFadeDuration,
Setters = { new Setter(Visual.OpacityProperty, 1d) }
}
KeyTime = TimeSpan.Zero,
Setters = { new Setter(Visual.OpacityProperty, 0d) }
},
new KeyFrame
{
KeyTime = MapBase.ImageFadeDuration,
Setters = { new Setter(Visual.OpacityProperty, 1d) }
}
};
}
};
_ = fadeInAnimation.RunAsync(Image);
}
_ = fadeInAnimation.RunAsync(Image);
}
await Image.Dispatcher.InvokeAsync(SetImageSource);
}
await Image.Dispatcher.InvokeAsync(SetImageSource);
}
}

View file

@ -1,14 +1,13 @@
using Avalonia.Animation;
namespace MapControl
namespace MapControl;
public class LocationAnimator : InterpolatingAnimator<Location>
{
public class LocationAnimator : InterpolatingAnimator<Location>
public override Location Interpolate(double progress, Location oldValue, Location newValue)
{
public override Location Interpolate(double progress, Location oldValue, Location newValue)
{
return new Location(
(1d - progress) * oldValue.Latitude + progress * newValue.Latitude,
(1d - progress) * oldValue.Longitude + progress * newValue.Longitude);
}
return new Location(
(1d - progress) * oldValue.Latitude + progress * newValue.Latitude,
(1d - progress) * oldValue.Longitude + progress * newValue.Longitude);
}
}

View file

@ -2,140 +2,139 @@
using Avalonia.Input;
using System;
namespace MapControl
namespace MapControl;
[Flags]
public enum ManipulationModes
{
[Flags]
public enum ManipulationModes
None = 0,
Translate = 1,
Rotate = 2,
Scale = 4,
All = Translate | Rotate | Scale
}
public partial class Map
{
public static readonly StyledProperty<ManipulationModes> ManipulationModeProperty =
DependencyPropertyHelper.Register<Map, ManipulationModes>(nameof(ManipulationMode), ManipulationModes.Translate | ManipulationModes.Scale);
/// <summary>
/// Gets or sets a value that specifies how the map control handles manipulations.
/// </summary>
public ManipulationModes ManipulationMode
{
None = 0,
Translate = 1,
Rotate = 2,
Scale = 4,
All = Translate | Rotate | Scale
get => GetValue(ManipulationModeProperty);
set => SetValue(ManipulationModeProperty, value);
}
public partial class Map
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
public static readonly StyledProperty<ManipulationModes> ManipulationModeProperty =
DependencyPropertyHelper.Register<Map, ManipulationModes>(nameof(ManipulationMode), ManipulationModes.Translate | ManipulationModes.Scale);
base.OnPointerWheelChanged(e);
/// <summary>
/// Gets or sets a value that specifies how the map control handles manipulations.
/// </summary>
public ManipulationModes ManipulationMode
OnMouseWheel(e.GetPosition(this), e.Delta.Y);
}
private IPointer pointer1;
private IPointer pointer2;
private Point position1;
private Point position2;
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
var point = e.GetCurrentPoint(this);
if (point.Pointer == pointer1 || point.Pointer == pointer2)
{
get => GetValue(ManipulationModeProperty);
set => SetValue(ManipulationModeProperty, value);
}
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
base.OnPointerWheelChanged(e);
OnMouseWheel(e.GetPosition(this), e.Delta.Y);
}
private IPointer pointer1;
private IPointer pointer2;
private Point position1;
private Point position2;
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
var point = e.GetCurrentPoint(this);
if (point.Pointer == pointer1 || point.Pointer == pointer2)
if (pointer2 != null)
{
if (pointer2 != null)
{
HandleManipulation(point.Pointer, point.Position);
}
else if (point.Pointer.Type == PointerType.Mouse ||
ManipulationMode.HasFlag(ManipulationModes.Translate))
{
TranslateMap(new Point(point.Position.X - position1.X, point.Position.Y - position1.Y));
position1 = point.Position;
}
HandleManipulation(point.Pointer, point.Position);
}
else if (pointer1 == null &&
point.Pointer.Type == PointerType.Mouse &&
point.Properties.IsLeftButtonPressed &&
e.KeyModifiers == KeyModifiers.None ||
pointer2 == null &&
point.Pointer.Type == PointerType.Touch &&
ManipulationMode != ManipulationModes.None)
else if (point.Pointer.Type == PointerType.Mouse ||
ManipulationMode.HasFlag(ManipulationModes.Translate))
{
point.Pointer.Capture(this);
if (pointer1 == null)
{
pointer1 = point.Pointer;
position1 = point.Position;
}
else
{
pointer2 = point.Pointer;
position2 = point.Position;
}
TranslateMap(new Point(point.Position.X - position1.X, point.Position.Y - position1.Y));
position1 = point.Position;
}
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
else if (pointer1 == null &&
point.Pointer.Type == PointerType.Mouse &&
point.Properties.IsLeftButtonPressed &&
e.KeyModifiers == KeyModifiers.None ||
pointer2 == null &&
point.Pointer.Type == PointerType.Touch &&
ManipulationMode != ManipulationModes.None)
{
base.OnPointerCaptureLost(e);
point.Pointer.Capture(this);
if (e.Pointer == pointer1 || e.Pointer == pointer2)
if (pointer1 == null)
{
if (e.Pointer == pointer1)
{
pointer1 = pointer2;
position1 = position2;
}
pointer2 = null;
}
}
private void HandleManipulation(IPointer pointer, Point position)
{
var oldDistance = new Vector(position2.X - position1.X, position2.Y - position1.Y);
var oldOrigin = new Point((position1.X + position2.X) / 2d, (position1.Y + position2.Y) / 2d);
if (pointer == pointer1)
{
position1 = position;
pointer1 = point.Pointer;
position1 = point.Position;
}
else
{
position2 = position;
pointer2 = point.Pointer;
position2 = point.Position;
}
var newDistance = new Vector(position2.X - position1.X, position2.Y - position1.Y);
var newOrigin = oldOrigin;
var translation = new Point();
var rotation = 0d;
var scale = 1d;
if (ManipulationMode.HasFlag(ManipulationModes.Translate))
{
newOrigin = new Point((position1.X + position2.X) / 2d, (position1.Y + position2.Y) / 2d);
translation = newOrigin - oldOrigin;
}
if (ManipulationMode.HasFlag(ManipulationModes.Rotate))
{
rotation = 180d / Math.PI *
(Math.Atan2(newDistance.Y, newDistance.X) - Math.Atan2(oldDistance.Y, oldDistance.X));
}
if (ManipulationMode.HasFlag(ManipulationModes.Scale))
{
scale = newDistance.Length / oldDistance.Length;
}
TransformMap(newOrigin, translation, rotation, scale);
}
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
base.OnPointerCaptureLost(e);
if (e.Pointer == pointer1 || e.Pointer == pointer2)
{
if (e.Pointer == pointer1)
{
pointer1 = pointer2;
position1 = position2;
}
pointer2 = null;
}
}
private void HandleManipulation(IPointer pointer, Point position)
{
var oldDistance = new Vector(position2.X - position1.X, position2.Y - position1.Y);
var oldOrigin = new Point((position1.X + position2.X) / 2d, (position1.Y + position2.Y) / 2d);
if (pointer == pointer1)
{
position1 = position;
}
else
{
position2 = position;
}
var newDistance = new Vector(position2.X - position1.X, position2.Y - position1.Y);
var newOrigin = oldOrigin;
var translation = new Point();
var rotation = 0d;
var scale = 1d;
if (ManipulationMode.HasFlag(ManipulationModes.Translate))
{
newOrigin = new Point((position1.X + position2.X) / 2d, (position1.Y + position2.Y) / 2d);
translation = newOrigin - oldOrigin;
}
if (ManipulationMode.HasFlag(ManipulationModes.Rotate))
{
rotation = 180d / Math.PI *
(Math.Atan2(newDistance.Y, newDistance.X) - Math.Atan2(oldDistance.Y, oldDistance.X));
}
if (ManipulationMode.HasFlag(ManipulationModes.Scale))
{
scale = newDistance.Length / oldDistance.Length;
}
TransformMap(newOrigin, translation, rotation, scale);
}
}

View file

@ -10,266 +10,265 @@ using System.Threading;
using System.Threading.Tasks;
using Brush = Avalonia.Media.IBrush;
namespace MapControl
namespace MapControl;
public partial class MapBase
{
public partial class MapBase
public static readonly StyledProperty<Brush> ForegroundProperty =
DependencyPropertyHelper.AddOwner<MapBase, Brush>(TextElement.ForegroundProperty, Brushes.Black);
public static readonly StyledProperty<Easing> AnimationEasingProperty =
DependencyPropertyHelper.Register<MapBase, Easing>(nameof(AnimationEasing), new QuadraticEaseOut());
public static readonly StyledProperty<Location> CenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(Center), new Location(0d, 0d),
(map, oldValue, newValue) => map.CenterPropertyChanged(newValue),
(map, value) => map.CoerceCenterProperty(value),
true);
public static readonly StyledProperty<Location> TargetCenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(0d, 0d),
async (map, oldValue, newValue) => await map.TargetCenterPropertyChanged(newValue),
(map, value) => map.CoerceCenterProperty(value),
true);
public static readonly StyledProperty<double> MinZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d,
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceMinZoomLevelProperty(value));
public static readonly StyledProperty<double> MaxZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d,
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceMaxZoomLevelProperty(value));
public static readonly StyledProperty<double> ZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d,
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceZoomLevelProperty(value),
true);
public static readonly StyledProperty<double> TargetZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d,
async (map, oldValue, newValue) => await map.TargetZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceZoomLevelProperty(value),
true);
public static readonly StyledProperty<double> HeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d,
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue),
(map, value) => map.CoerceHeadingProperty(value),
true);
public static readonly StyledProperty<double> TargetHeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d,
async (map, oldValue, newValue) => await map.TargetHeadingPropertyChanged(newValue),
(map, value) => map.CoerceHeadingProperty(value),
true);
public static readonly DirectProperty<MapBase, double> ViewScaleProperty =
AvaloniaProperty.RegisterDirect<MapBase, double>(nameof(ViewScale), map => map.ViewTransform.Scale);
private CancellationTokenSource centerCts;
private CancellationTokenSource zoomLevelCts;
private CancellationTokenSource headingCts;
private Animation centerAnimation;
private Animation zoomLevelAnimation;
private Animation headingAnimation;
static MapBase()
{
public static readonly StyledProperty<Brush> ForegroundProperty =
DependencyPropertyHelper.AddOwner<MapBase, Brush>(TextElement.ForegroundProperty, Brushes.Black);
BackgroundProperty.OverrideDefaultValue<MapBase>(Brushes.White);
ClipToBoundsProperty.OverrideDefaultValue<MapBase>(true);
public static readonly StyledProperty<Easing> AnimationEasingProperty =
DependencyPropertyHelper.Register<MapBase, Easing>(nameof(AnimationEasing), new QuadraticEaseOut());
Animation.RegisterCustomAnimator<Location, LocationAnimator>();
}
public static readonly StyledProperty<Location> CenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(Center), new Location(0d, 0d),
(map, oldValue, newValue) => map.CenterPropertyChanged(newValue),
(map, value) => map.CoerceCenterProperty(value),
true);
public double ActualWidth => Bounds.Width;
public double ActualHeight => Bounds.Height;
public static readonly StyledProperty<Location> TargetCenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(0d, 0d),
async (map, oldValue, newValue) => await map.TargetCenterPropertyChanged(newValue),
(map, value) => map.CoerceCenterProperty(value),
true);
protected override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
public static readonly StyledProperty<double> MinZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d,
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceMinZoomLevelProperty(value));
ResetTransformCenter();
UpdateTransform();
}
public static readonly StyledProperty<double> MaxZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d,
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceMaxZoomLevelProperty(value));
/// <summary>
/// Gets or sets the Easing of the Center, ZoomLevel and Heading animations.
/// The default value is a QuadraticEaseOut.
/// </summary>
public Easing AnimationEasing
{
get => GetValue(AnimationEasingProperty);
set => SetValue(AnimationEasingProperty, value);
}
public static readonly StyledProperty<double> ZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d,
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceZoomLevelProperty(value),
true);
/// <summary>
/// Gets the scaling factor from projected map coordinates to view coordinates,
/// as pixels per meter.
/// </summary>
public double ViewScale
{
get => GetValue(ViewScaleProperty);
private set => RaisePropertyChanged(ViewScaleProperty, double.NaN, value);
}
public static readonly StyledProperty<double> TargetZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d,
async (map, oldValue, newValue) => await map.TargetZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceZoomLevelProperty(value),
true);
public static readonly StyledProperty<double> HeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d,
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue),
(map, value) => map.CoerceHeadingProperty(value),
true);
public static readonly StyledProperty<double> TargetHeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d,
async (map, oldValue, newValue) => await map.TargetHeadingPropertyChanged(newValue),
(map, value) => map.CoerceHeadingProperty(value),
true);
public static readonly DirectProperty<MapBase, double> ViewScaleProperty =
AvaloniaProperty.RegisterDirect<MapBase, double>(nameof(ViewScale), map => map.ViewTransform.Scale);
private CancellationTokenSource centerCts;
private CancellationTokenSource zoomLevelCts;
private CancellationTokenSource headingCts;
private Animation centerAnimation;
private Animation zoomLevelAnimation;
private Animation headingAnimation;
static MapBase()
private void CenterPropertyChanged(Location center)
{
if (!internalPropertyChange)
{
BackgroundProperty.OverrideDefaultValue<MapBase>(Brushes.White);
ClipToBoundsProperty.OverrideDefaultValue<MapBase>(true);
Animation.RegisterCustomAnimator<Location, LocationAnimator>();
}
public double ActualWidth => Bounds.Width;
public double ActualHeight => Bounds.Height;
protected override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
ResetTransformCenter();
UpdateTransform();
}
/// <summary>
/// Gets or sets the Easing of the Center, ZoomLevel and Heading animations.
/// The default value is a QuadraticEaseOut.
/// </summary>
public Easing AnimationEasing
{
get => GetValue(AnimationEasingProperty);
set => SetValue(AnimationEasingProperty, value);
}
/// <summary>
/// Gets the scaling factor from projected map coordinates to view coordinates,
/// as pixels per meter.
/// </summary>
public double ViewScale
{
get => GetValue(ViewScaleProperty);
private set => RaisePropertyChanged(ViewScaleProperty, double.NaN, value);
}
private void CenterPropertyChanged(Location center)
{
if (!internalPropertyChange)
if (centerAnimation == null)
{
UpdateTransform();
if (centerAnimation == null)
{
SetValueInternal(TargetCenterProperty, center);
}
SetValueInternal(TargetCenterProperty, center);
}
}
private async Task TargetCenterPropertyChanged(Location targetCenter)
{
if (!internalPropertyChange && !targetCenter.Equals(Center))
{
ResetTransformCenter();
centerCts?.Cancel();
centerAnimation = CreateAnimation(CenterProperty, new Location(targetCenter.Latitude, NearestLongitude(targetCenter.Longitude)));
using (centerCts = new CancellationTokenSource())
{
await centerAnimation.RunAsync(this, centerCts.Token);
if (!centerCts.IsCancellationRequested)
{
UpdateTransform();
}
}
centerCts = null;
centerAnimation = null;
}
}
private void MinZoomLevelPropertyChanged(double minZoomLevel)
{
if (ZoomLevel < minZoomLevel)
{
ZoomLevel = minZoomLevel;
}
}
private void MaxZoomLevelPropertyChanged(double maxZoomLevel)
{
if (ZoomLevel > maxZoomLevel)
{
ZoomLevel = maxZoomLevel;
}
}
private void ZoomLevelPropertyChanged(double zoomLevel)
{
if (!internalPropertyChange)
{
UpdateTransform();
if (zoomLevelAnimation == null)
{
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
}
}
}
private async Task TargetZoomLevelPropertyChanged(double targetZoomLevel)
{
if (!internalPropertyChange && targetZoomLevel != ZoomLevel)
{
zoomLevelCts?.Cancel();
zoomLevelAnimation = CreateAnimation(ZoomLevelProperty, targetZoomLevel);
using (zoomLevelCts = new CancellationTokenSource())
{
await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token);
if (!zoomLevelCts.IsCancellationRequested)
{
UpdateTransform(true); // reset transform center
}
}
zoomLevelCts = null;
zoomLevelAnimation = null;
}
}
private void HeadingPropertyChanged(double heading)
{
if (!internalPropertyChange)
{
UpdateTransform();
if (headingAnimation == null)
{
SetValueInternal(TargetHeadingProperty, heading);
}
}
}
private async Task TargetHeadingPropertyChanged(double targetHeading)
{
if (!internalPropertyChange && targetHeading != Heading)
{
var delta = targetHeading - Heading;
if (delta > 180d)
{
delta -= 360d;
}
else if (delta < -180d)
{
delta += 360d;
}
targetHeading = Heading + delta;
headingCts?.Cancel();
headingAnimation = CreateAnimation(HeadingProperty, targetHeading);
using (headingCts = new CancellationTokenSource())
{
await headingAnimation.RunAsync(this, headingCts.Token);
if (!headingCts.IsCancellationRequested)
{
UpdateTransform();
}
}
headingCts = null;
headingAnimation = null;
}
}
private Animation CreateAnimation(DependencyProperty property, object value)
{
return new Animation
{
FillMode = FillMode.Forward,
Duration = AnimationDuration,
Easing = AnimationEasing,
Children =
{
new KeyFrame
{
KeyTime = AnimationDuration,
Setters = { new Setter(property, value) }
}
}
};
}
}
private async Task TargetCenterPropertyChanged(Location targetCenter)
{
if (!internalPropertyChange && !targetCenter.Equals(Center))
{
ResetTransformCenter();
centerCts?.Cancel();
centerAnimation = CreateAnimation(CenterProperty, new Location(targetCenter.Latitude, NearestLongitude(targetCenter.Longitude)));
using (centerCts = new CancellationTokenSource())
{
await centerAnimation.RunAsync(this, centerCts.Token);
if (!centerCts.IsCancellationRequested)
{
UpdateTransform();
}
}
centerCts = null;
centerAnimation = null;
}
}
private void MinZoomLevelPropertyChanged(double minZoomLevel)
{
if (ZoomLevel < minZoomLevel)
{
ZoomLevel = minZoomLevel;
}
}
private void MaxZoomLevelPropertyChanged(double maxZoomLevel)
{
if (ZoomLevel > maxZoomLevel)
{
ZoomLevel = maxZoomLevel;
}
}
private void ZoomLevelPropertyChanged(double zoomLevel)
{
if (!internalPropertyChange)
{
UpdateTransform();
if (zoomLevelAnimation == null)
{
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
}
}
}
private async Task TargetZoomLevelPropertyChanged(double targetZoomLevel)
{
if (!internalPropertyChange && targetZoomLevel != ZoomLevel)
{
zoomLevelCts?.Cancel();
zoomLevelAnimation = CreateAnimation(ZoomLevelProperty, targetZoomLevel);
using (zoomLevelCts = new CancellationTokenSource())
{
await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token);
if (!zoomLevelCts.IsCancellationRequested)
{
UpdateTransform(true); // reset transform center
}
}
zoomLevelCts = null;
zoomLevelAnimation = null;
}
}
private void HeadingPropertyChanged(double heading)
{
if (!internalPropertyChange)
{
UpdateTransform();
if (headingAnimation == null)
{
SetValueInternal(TargetHeadingProperty, heading);
}
}
}
private async Task TargetHeadingPropertyChanged(double targetHeading)
{
if (!internalPropertyChange && targetHeading != Heading)
{
var delta = targetHeading - Heading;
if (delta > 180d)
{
delta -= 360d;
}
else if (delta < -180d)
{
delta += 360d;
}
targetHeading = Heading + delta;
headingCts?.Cancel();
headingAnimation = CreateAnimation(HeadingProperty, targetHeading);
using (headingCts = new CancellationTokenSource())
{
await headingAnimation.RunAsync(this, headingCts.Token);
if (!headingCts.IsCancellationRequested)
{
UpdateTransform();
}
}
headingCts = null;
headingAnimation = null;
}
}
private Animation CreateAnimation(DependencyProperty property, object value)
{
return new Animation
{
FillMode = FillMode.Forward,
Duration = AnimationDuration,
Easing = AnimationEasing,
Children =
{
new KeyFrame
{
KeyTime = AnimationDuration,
Setters = { new Setter(property, value) }
}
}
};
}
}

View file

@ -1,6 +0,0 @@
using Avalonia;
using Avalonia.Controls;
namespace MapControl
{
}

View file

@ -7,116 +7,115 @@ using System;
using System.Collections.Generic;
using System.Globalization;
namespace MapControl
namespace MapControl;
public partial class MapGrid : Control, IMapElement
{
public partial class MapGrid : Control, IMapElement
static MapGrid()
{
static MapGrid()
AffectsRender<MapGrid>(ForegroundProperty);
}
public static readonly StyledProperty<IBrush> ForegroundProperty =
DependencyPropertyHelper.AddOwner<MapGrid, IBrush>(TextElement.ForegroundProperty);
public static readonly StyledProperty<FontFamily> FontFamilyProperty =
DependencyPropertyHelper.AddOwner<MapGrid, FontFamily>(TextElement.FontFamilyProperty);
public static readonly StyledProperty<double> FontSizeProperty =
DependencyPropertyHelper.AddOwner<MapGrid, double>(TextElement.FontSizeProperty, 12d);
/// <summary>
/// Implements IMapElement.ParentMap.
/// </summary>
public MapBase ParentMap
{
get;
set
{
AffectsRender<MapGrid>(ForegroundProperty);
}
public static readonly StyledProperty<IBrush> ForegroundProperty =
DependencyPropertyHelper.AddOwner<MapGrid, IBrush>(TextElement.ForegroundProperty);
public static readonly StyledProperty<FontFamily> FontFamilyProperty =
DependencyPropertyHelper.AddOwner<MapGrid, FontFamily>(TextElement.FontFamilyProperty);
public static readonly StyledProperty<double> FontSizeProperty =
DependencyPropertyHelper.AddOwner<MapGrid, double>(TextElement.FontSizeProperty, 12d);
/// <summary>
/// Implements IMapElement.ParentMap.
/// </summary>
public MapBase ParentMap
{
get;
set
if (field != null)
{
if (field != null)
{
field.ViewportChanged -= OnViewportChanged;
}
field.ViewportChanged -= OnViewportChanged;
}
field = value;
field = value;
if (field != null)
{
field.ViewportChanged += OnViewportChanged;
}
if (field != null)
{
field.ViewportChanged += OnViewportChanged;
}
}
}
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
{
OnViewportChanged(e);
}
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
{
OnViewportChanged(e);
}
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
{
InvalidateVisual();
}
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
{
InvalidateVisual();
}
public override void Render(DrawingContext drawingContext)
public override void Render(DrawingContext drawingContext)
{
if (ParentMap != null)
{
if (ParentMap != null)
var pathGeometry = new PathGeometry();
var labels = new List<Label>();
var pen = new Pen
{
var pathGeometry = new PathGeometry();
var labels = new List<Label>();
var pen = new Pen
Brush = Foreground,
Thickness = StrokeThickness,
};
DrawGrid(pathGeometry.Figures, labels);
drawingContext.DrawGeometry(null, pen, pathGeometry);
if (labels.Count > 0)
{
var typeface = new Typeface(FontFamily, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal);
foreach (var label in labels)
{
Brush = Foreground,
Thickness = StrokeThickness,
};
var text = new FormattedText(label.Text,
CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, FontSize, Foreground);
var x = label.X +
label.HorizontalAlignment switch
{
HorizontalAlignment.Left => 2d,
HorizontalAlignment.Right => -text.Width - 2d,
_ => -text.Width / 2d
};
var y = label.Y +
label.VerticalAlignment switch
{
VerticalAlignment.Top => 0,
VerticalAlignment.Bottom => -text.Height,
_ => -text.Height / 2d
};
DrawGrid(pathGeometry.Figures, labels);
drawingContext.DrawGeometry(null, pen, pathGeometry);
if (labels.Count > 0)
{
var typeface = new Typeface(FontFamily, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal);
foreach (var label in labels)
if (label.Rotation != 0d)
{
var text = new FormattedText(label.Text,
CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, FontSize, Foreground);
var x = label.X +
label.HorizontalAlignment switch
{
HorizontalAlignment.Left => 2d,
HorizontalAlignment.Right => -text.Width - 2d,
_ => -text.Width / 2d
};
var y = label.Y +
label.VerticalAlignment switch
{
VerticalAlignment.Top => 0,
VerticalAlignment.Bottom => -text.Height,
_ => -text.Height / 2d
};
var transform = Avalonia.Matrix.CreateRotation(
label.Rotation * Math.PI / 180d, new Point(label.X, label.Y));
if (label.Rotation != 0d)
{
var transform = Avalonia.Matrix.CreateRotation(
label.Rotation * Math.PI / 180d, new Point(label.X, label.Y));
using var pushedState = drawingContext.PushTransform(transform);
using var pushedState = drawingContext.PushTransform(transform);
drawingContext.DrawText(text, new Point(x, y));
}
else
{
drawingContext.DrawText(text, new Point(x, y));
}
drawingContext.DrawText(text, new Point(x, y));
}
else
{
drawingContext.DrawText(text, new Point(x, y));
}
}
}
}
}
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
{
return new PolyLineSegment(points);
}
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
{
return new PolyLineSegment(points);
}
}

View file

@ -2,29 +2,28 @@
using Avalonia.Styling;
using System.Threading.Tasks;
namespace MapControl
{
public partial class MapImageLayer
{
private void FadeOver()
{
var fadeInAnimation = new Animation
{
FillMode = FillMode.Forward,
Duration = MapBase.ImageFadeDuration,
Children =
{
new KeyFrame
{
KeyTime = MapBase.ImageFadeDuration,
Setters = { new Setter(OpacityProperty, 1d) }
}
}
};
namespace MapControl;
_ = fadeInAnimation.RunAsync(Children[1]).ContinueWith(
_ => Children[0].Opacity = 0d,
TaskScheduler.FromCurrentSynchronizationContext());
}
public partial class MapImageLayer
{
private void FadeOver()
{
var fadeInAnimation = new Animation
{
FillMode = FillMode.Forward,
Duration = MapBase.ImageFadeDuration,
Children =
{
new KeyFrame
{
KeyTime = MapBase.ImageFadeDuration,
Setters = { new Setter(OpacityProperty, 1d) }
}
}
};
_ = fadeInAnimation.RunAsync(Children[1]).ContinueWith(
_ => Children[0].Opacity = 0d,
TaskScheduler.FromCurrentSynchronizationContext());
}
}

View file

@ -1,31 +1,30 @@
using Avalonia.Controls;
using Avalonia.Input;
namespace MapControl
namespace MapControl;
public partial class MapItem
{
public partial class MapItem
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
protected override void OnPointerPressed(PointerPressedEventArgs e)
if (e.Pointer.Type != PointerType.Mouse &&
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl)
{
if (e.Pointer.Type != PointerType.Mouse &&
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl)
{
mapItemsControl.UpdateSelectionFromEvent(this, e);
}
e.Handled = true;
mapItemsControl.UpdateSelectionFromEvent(this, e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (e.Pointer.Type == PointerType.Mouse &&
e.InitialPressMouseButton == MouseButton.Left &&
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl)
{
mapItemsControl.UpdateSelectionFromEvent(this, e);
}
e.Handled = true;
}
e.Handled = true;
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (e.Pointer.Type == PointerType.Mouse &&
e.InitialPressMouseButton == MouseButton.Left &&
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl)
{
mapItemsControl.UpdateSelectionFromEvent(this, e);
}
e.Handled = true;
}
}

View file

@ -6,70 +6,69 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
namespace MapControl
namespace MapControl;
public partial class MapItemsControl
{
public partial class MapItemsControl
static MapItemsControl()
{
static MapItemsControl()
{
TemplateProperty.OverrideDefaultValue<MapItemsControl>(
new FuncControlTemplate<MapItemsControl>(
(itemsControl, namescope) => new ItemsPresenter { ItemsPanel = itemsControl.ItemsPanel }));
ItemsPanelProperty.OverrideDefaultValue<MapItemsControl>(
new FuncTemplate<Panel>(() => new MapPanel()));
}
public void SelectItemsInGeometry(Geometry geometry)
{
SelectItemsByPosition(geometry.FillContains);
}
public new MapItem ContainerFromItem(object item)
{
return (MapItem)base.ContainerFromItem(item);
}
protected override bool NeedsContainerOverride(object item, int index, out object recycleKey)
{
recycleKey = null;
return item is not MapItem;
}
protected override Control CreateContainerForItemOverride(object item, int index, object recycleKey)
{
return new MapItem();
}
protected override void PrepareContainerForItemOverride(Control container, object item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
PrepareContainer((MapItem)container, item);
}
protected override void ClearContainerForItemOverride(Control container)
{
base.ClearContainerForItemOverride(container);
ClearContainer((MapItem)container);
}
protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs)
TemplateProperty.OverrideDefaultValue<MapItemsControl>(
new FuncControlTemplate<MapItemsControl>(
(itemsControl, namescope) => new ItemsPresenter { ItemsPanel = itemsControl.ItemsPanel }));
ItemsPanelProperty.OverrideDefaultValue<MapItemsControl>(
new FuncTemplate<Panel>(() => new MapPanel()));
}
public void SelectItemsInGeometry(Geometry geometry)
{
SelectItemsByPosition(geometry.FillContains);
}
public new MapItem ContainerFromItem(object item)
{
return (MapItem)base.ContainerFromItem(item);
}
protected override bool NeedsContainerOverride(object item, int index, out object recycleKey)
{
recycleKey = null;
return item is not MapItem;
}
protected override Control CreateContainerForItemOverride(object item, int index, object recycleKey)
{
return new MapItem();
}
protected override void PrepareContainerForItemOverride(Control container, object item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
PrepareContainer((MapItem)container, item);
}
protected override void ClearContainerForItemOverride(Control container)
{
base.ClearContainerForItemOverride(container);
ClearContainer((MapItem)container);
}
protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs)
{
return true;
}
public override bool UpdateSelectionFromEvent(UIElement container, RoutedEventArgs eventArgs)
{
if (SelectionMode == SelectionMode.Multiple &&
eventArgs is PointerEventArgs e &&
e.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
SelectItemsInRange((MapItem)container);
return true;
}
public override bool UpdateSelectionFromEvent(UIElement container, RoutedEventArgs eventArgs)
{
if (SelectionMode == SelectionMode.Multiple &&
eventArgs is PointerEventArgs e &&
e.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
SelectItemsInRange((MapItem)container);
return true;
}
return base.UpdateSelectionFromEvent(container, eventArgs);
}
return base.UpdateSelectionFromEvent(container, eventArgs);
}
}

View file

@ -1,29 +1,28 @@
using Avalonia;
namespace MapControl
namespace MapControl;
public partial class MapPanel
{
public partial class MapPanel
public static readonly AttachedProperty<bool> AutoCollapseProperty =
DependencyPropertyHelper.RegisterAttached<bool>("AutoCollapse", typeof(MapPanel));
public static readonly AttachedProperty<Location> LocationProperty =
DependencyPropertyHelper.RegisterAttached<Location>("Location", typeof(MapPanel));
public static readonly AttachedProperty<BoundingBox> BoundingBoxProperty =
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel));
public static readonly AttachedProperty<Rect?> MapRectProperty =
DependencyPropertyHelper.RegisterAttached<Rect?>("MapRect", typeof(MapPanel));
static MapPanel()
{
public static readonly AttachedProperty<bool> AutoCollapseProperty =
DependencyPropertyHelper.RegisterAttached<bool>("AutoCollapse", typeof(MapPanel));
AffectsParentArrange<MapPanel>(LocationProperty, BoundingBoxProperty, MapRectProperty);
}
public static readonly AttachedProperty<Location> LocationProperty =
DependencyPropertyHelper.RegisterAttached<Location>("Location", typeof(MapPanel));
public static readonly AttachedProperty<BoundingBox> BoundingBoxProperty =
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel));
public static readonly AttachedProperty<Rect?> MapRectProperty =
DependencyPropertyHelper.RegisterAttached<Rect?>("MapRect", typeof(MapPanel));
static MapPanel()
{
AffectsParentArrange<MapPanel>(LocationProperty, BoundingBoxProperty, MapRectProperty);
}
public static MapBase GetParentMap(FrameworkElement element)
{
return (MapBase)element.GetValue(ParentMapProperty);
}
public static MapBase GetParentMap(FrameworkElement element)
{
return (MapBase)element.GetValue(ParentMapProperty);
}
}

View file

@ -2,20 +2,19 @@
using Avalonia.Controls.Shapes;
using Avalonia.Media;
namespace MapControl
namespace MapControl;
public partial class MapPath : Shape
{
public partial class MapPath : Shape
public static readonly StyledProperty<Geometry> DataProperty =
DependencyPropertyHelper.AddOwner<MapPath, Geometry>(Path.DataProperty,
(path, oldValue, newValue) => path.UpdateData());
public Geometry Data
{
public static readonly StyledProperty<Geometry> DataProperty =
DependencyPropertyHelper.AddOwner<MapPath, Geometry>(Path.DataProperty,
(path, oldValue, newValue) => path.UpdateData());
public Geometry Data
{
get => GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
protected override Geometry CreateDefiningGeometry() => Data;
get => GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
protected override Geometry CreateDefiningGeometry() => Data;
}

View file

@ -4,89 +4,88 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace MapControl
namespace MapControl;
public partial class MapPolypoint : MapPath
{
public partial class MapPolypoint : MapPath
protected void UpdateData(IEnumerable<Location> locations, bool closed)
{
protected void UpdateData(IEnumerable<Location> locations, bool closed)
var figures = new PathFigures();
if (ParentMap != null && locations != null)
{
var figures = new PathFigures();
var longitudeOffset = GetLongitudeOffset(locations);
if (ParentMap != null && locations != null)
{
var longitudeOffset = GetLongitudeOffset(locations);
AddPolylinePoints(figures, locations, longitudeOffset, closed);
}
SetPathFigures(figures);
AddPolylinePoints(figures, locations, longitudeOffset, closed);
}
protected void UpdateData(IEnumerable<IEnumerable<Location>> polygons)
SetPathFigures(figures);
}
protected void UpdateData(IEnumerable<IEnumerable<Location>> polygons)
{
var figures = new PathFigures();
if (ParentMap != null && polygons != null)
{
var figures = new PathFigures();
var longitudeOffset = GetLongitudeOffset(polygons.FirstOrDefault());
if (ParentMap != null && polygons != null)
foreach (var locations in polygons)
{
var longitudeOffset = GetLongitudeOffset(polygons.FirstOrDefault());
foreach (var locations in polygons)
{
AddPolylinePoints(figures, locations, longitudeOffset, true);
}
}
SetPathFigures(figures);
}
private void AddPolylinePoints(PathFigures figures, IEnumerable<Location> locations, double longitudeOffset, bool closed)
{
var points = locations.Select(location => LocationToView(location, longitudeOffset));
if (points.Any())
{
var start = points.First();
var polyline = new PolyLineSegment(points.Skip(1));
var minX = start.X;
var maxX = start.X;
var minY = start.Y;
var maxY = start.Y;
foreach (var point in polyline.Points)
{
minX = Math.Min(minX, point.X);
maxX = Math.Max(maxX, point.X);
minY = Math.Min(minY, point.Y);
maxY = Math.Max(maxY, point.Y);
}
if (maxX >= 0d && minX <= ParentMap.ActualWidth &&
maxY >= 0d && minY <= ParentMap.ActualHeight)
{
var figure = new PathFigure
{
StartPoint = start,
IsClosed = closed,
IsFilled = true
};
figure.Segments.Add(polyline);
figures.Add(figure);
}
AddPolylinePoints(figures, locations, longitudeOffset, true);
}
}
private void SetPathFigures(PathFigures figures)
SetPathFigures(figures);
}
private void AddPolylinePoints(PathFigures figures, IEnumerable<Location> locations, double longitudeOffset, bool closed)
{
var points = locations.Select(location => LocationToView(location, longitudeOffset));
if (points.Any())
{
if (figures.Count == 0)
var start = points.First();
var polyline = new PolyLineSegment(points.Skip(1));
var minX = start.X;
var maxX = start.X;
var minY = start.Y;
var maxY = start.Y;
foreach (var point in polyline.Points)
{
// Avalonia Shape seems to ignore PathGeometry with empty Figures collection.
//
figures.Add(new PathFigure { StartPoint = new Point(-1000, -1000) });
minX = Math.Min(minX, point.X);
maxX = Math.Max(maxX, point.X);
minY = Math.Min(minY, point.Y);
maxY = Math.Max(maxY, point.Y);
}
((PathGeometry)Data).Figures = figures;
InvalidateGeometry();
if (maxX >= 0d && minX <= ParentMap.ActualWidth &&
maxY >= 0d && minY <= ParentMap.ActualHeight)
{
var figure = new PathFigure
{
StartPoint = start,
IsClosed = closed,
IsFilled = true
};
figure.Segments.Add(polyline);
figures.Add(figure);
}
}
}
private void SetPathFigures(PathFigures figures)
{
if (figures.Count == 0)
{
// Avalonia Shape seems to ignore PathGeometry with empty Figures collection.
//
figures.Add(new PathFigure { StartPoint = new Point(-1000, -1000) });
}
((PathGeometry)Data).Figures = figures;
InvalidateGeometry();
}
}

View file

@ -4,100 +4,99 @@ using Avalonia.Media;
using System;
using Brush = Avalonia.Media.IBrush;
namespace MapControl
namespace MapControl;
public partial class PushpinBorder : Decorator
{
public partial class PushpinBorder : Decorator
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
DependencyPropertyHelper.Register<PushpinBorder, CornerRadius>(nameof(CornerRadius), new CornerRadius());
public static readonly StyledProperty<Size> ArrowSizeProperty =
DependencyPropertyHelper.Register<PushpinBorder, Size>(nameof(ArrowSize), new Size(10d, 20d));
public static readonly StyledProperty<double> BorderWidthProperty =
DependencyPropertyHelper.Register<PushpinBorder, double>(nameof(BorderWidth));
public static readonly StyledProperty<Brush> BackgroundProperty =
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(Background));
public static readonly StyledProperty<Brush> BorderBrushProperty =
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(BorderBrush));
static PushpinBorder()
{
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
DependencyPropertyHelper.Register<PushpinBorder, CornerRadius>(nameof(CornerRadius), new CornerRadius());
AffectsMeasure<PushpinBorder>(ArrowSizeProperty, BorderWidthProperty, CornerRadiusProperty);
AffectsRender<PushpinBorder>(BackgroundProperty, BorderBrushProperty);
}
public static readonly StyledProperty<Size> ArrowSizeProperty =
DependencyPropertyHelper.Register<PushpinBorder, Size>(nameof(ArrowSize), new Size(10d, 20d));
public double ActualWidth => Bounds.Width;
public double ActualHeight => Bounds.Height;
public static readonly StyledProperty<double> BorderWidthProperty =
DependencyPropertyHelper.Register<PushpinBorder, double>(nameof(BorderWidth));
public CornerRadius CornerRadius
{
get => GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public static readonly StyledProperty<Brush> BackgroundProperty =
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(Background));
public Brush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public static readonly StyledProperty<Brush> BorderBrushProperty =
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(BorderBrush));
public Brush BorderBrush
{
get => GetValue(BorderBrushProperty);
set => SetValue(BorderBrushProperty, value);
}
static PushpinBorder()
protected override Size MeasureOverride(Size constraint)
{
var width = 2d * BorderWidth + Padding.Left + Padding.Right;
var height = 2d * BorderWidth + Padding.Top + Padding.Bottom;
if (Child != null)
{
AffectsMeasure<PushpinBorder>(ArrowSizeProperty, BorderWidthProperty, CornerRadiusProperty);
AffectsRender<PushpinBorder>(BackgroundProperty, BorderBrushProperty);
Child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
width += Child.DesiredSize.Width;
height += Child.DesiredSize.Height;
}
public double ActualWidth => Bounds.Width;
public double ActualHeight => Bounds.Height;
var minWidth = BorderWidth + Math.Max(
CornerRadius.TopLeft + CornerRadius.TopRight,
CornerRadius.BottomLeft + CornerRadius.BottomRight + ArrowSize.Width);
public CornerRadius CornerRadius
var minHeight = BorderWidth + Math.Max(
CornerRadius.TopLeft + CornerRadius.BottomLeft,
CornerRadius.TopRight + CornerRadius.BottomRight);
return new Size(
Math.Max(width, minWidth),
Math.Max(height, minHeight) + ArrowSize.Height);
}
protected override Size ArrangeOverride(Size size)
{
Child?.Arrange(new Rect(
BorderWidth + Padding.Left,
BorderWidth + Padding.Top,
Child.DesiredSize.Width,
Child.DesiredSize.Height));
return DesiredSize;
}
public override void Render(DrawingContext drawingContext)
{
var pen = new Pen
{
get => GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
Brush = BorderBrush,
Thickness = BorderWidth,
LineJoin = PenLineJoin.Round
};
public Brush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
drawingContext.DrawGeometry(Background, pen, BuildGeometry());
public Brush BorderBrush
{
get => GetValue(BorderBrushProperty);
set => SetValue(BorderBrushProperty, value);
}
protected override Size MeasureOverride(Size constraint)
{
var width = 2d * BorderWidth + Padding.Left + Padding.Right;
var height = 2d * BorderWidth + Padding.Top + Padding.Bottom;
if (Child != null)
{
Child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
width += Child.DesiredSize.Width;
height += Child.DesiredSize.Height;
}
var minWidth = BorderWidth + Math.Max(
CornerRadius.TopLeft + CornerRadius.TopRight,
CornerRadius.BottomLeft + CornerRadius.BottomRight + ArrowSize.Width);
var minHeight = BorderWidth + Math.Max(
CornerRadius.TopLeft + CornerRadius.BottomLeft,
CornerRadius.TopRight + CornerRadius.BottomRight);
return new Size(
Math.Max(width, minWidth),
Math.Max(height, minHeight) + ArrowSize.Height);
}
protected override Size ArrangeOverride(Size size)
{
Child?.Arrange(new Rect(
BorderWidth + Padding.Left,
BorderWidth + Padding.Top,
Child.DesiredSize.Width,
Child.DesiredSize.Height));
return DesiredSize;
}
public override void Render(DrawingContext drawingContext)
{
var pen = new Pen
{
Brush = BorderBrush,
Thickness = BorderWidth,
LineJoin = PenLineJoin.Round
};
drawingContext.DrawGeometry(Background, pen, BuildGeometry());
base.Render(drawingContext);
}
base.Render(drawingContext);
}
}

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

View file

@ -6,169 +6,168 @@ using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace MapControl
namespace MapControl;
public class BitmapTile(int zoomLevel, int x, int y, int columnCount, int width, int height)
: Tile(zoomLevel, x, y, columnCount)
{
public class BitmapTile(int zoomLevel, int x, int y, int columnCount, int width, int height)
: Tile(zoomLevel, x, y, columnCount)
public event EventHandler Completed;
public byte[] PixelBuffer { get; set; }
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
{
public event EventHandler Completed;
var image = await loadImageFunc().ConfigureAwait(false);
public byte[] PixelBuffer { get; set; }
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
if (image is BitmapSource bitmap)
{
var image = await loadImageFunc().ConfigureAwait(false);
if (image is BitmapSource bitmap)
if (bitmap.Format != PixelFormats.Pbgra32)
{
if (bitmap.Format != PixelFormats.Pbgra32)
{
bitmap = new FormatConvertedBitmap(bitmap, PixelFormats.Pbgra32, null, 0d);
}
PixelBuffer = new byte[4 * width * height];
bitmap.CopyPixels(PixelBuffer, 4 * width, 0);
bitmap = new FormatConvertedBitmap(bitmap, PixelFormats.Pbgra32, null, 0d);
}
Completed?.Invoke(this, EventArgs.Empty);
PixelBuffer = new byte[4 * width * height];
bitmap.CopyPixels(PixelBuffer, 4 * width, 0);
}
Completed?.Invoke(this, EventArgs.Empty);
}
}
public class BitmapTileMatrixLayer(WmtsTileMatrix wmtsTileMatrix, int zoomLevel) : UIElement
{
private readonly MatrixTransform transform = new MatrixTransform();
private WriteableBitmap bitmap;
public WmtsTileMatrix WmtsTileMatrix => wmtsTileMatrix;
public TileMatrix TileMatrix { get; private set; } = new TileMatrix(zoomLevel, 1, 1, 0, 0);
public IEnumerable<BitmapTile> Tiles { get; private set; } = [];
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.PushTransform(transform);
drawingContext.DrawImage(bitmap, new Rect(0d, 0d, bitmap.PixelWidth, bitmap.PixelHeight));
}
public class BitmapTileMatrixLayer(WmtsTileMatrix wmtsTileMatrix, int zoomLevel) : UIElement
public void UpdateRenderTransform(ViewTransform viewTransform)
{
private readonly MatrixTransform transform = new MatrixTransform();
// Tile matrix origin in pixels.
//
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
private WriteableBitmap bitmap;
transform.Matrix = viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
}
public WmtsTileMatrix WmtsTileMatrix => wmtsTileMatrix;
public bool UpdateTiles(ViewTransform viewTransform, double viewWidth, double viewHeight)
{
// Tile matrix bounds in pixels.
//
var bounds = viewTransform.GetTileMatrixBounds(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, viewWidth, viewHeight);
public TileMatrix TileMatrix { get; private set; } = new TileMatrix(zoomLevel, 1, 1, 0, 0);
// Tile X and Y bounds.
//
var xMin = (int)Math.Floor(bounds.X / WmtsTileMatrix.TileWidth);
var yMin = (int)Math.Floor(bounds.Y / WmtsTileMatrix.TileHeight);
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / WmtsTileMatrix.TileWidth);
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / WmtsTileMatrix.TileHeight);
public IEnumerable<BitmapTile> Tiles { get; private set; } = [];
protected override void OnRender(DrawingContext drawingContext)
if (!WmtsTileMatrix.HasFullHorizontalCoverage)
{
drawingContext.PushTransform(transform);
drawingContext.DrawImage(bitmap, new Rect(0d, 0d, bitmap.PixelWidth, bitmap.PixelHeight));
// Set X range limits.
//
xMin = Math.Max(xMin, 0);
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
}
public void UpdateRenderTransform(ViewTransform viewTransform)
{
// Tile matrix origin in pixels.
//
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
// Set Y range limits.
//
yMin = Math.Max(yMin, 0);
yMax = Math.Min(Math.Max(yMax, 0), WmtsTileMatrix.MatrixHeight - 1);
transform.Matrix = viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
if (TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
{
// No change of the TileMatrix and the Tiles collection.
//
return false;
}
public bool UpdateTiles(ViewTransform viewTransform, double viewWidth, double viewHeight)
TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax);
bitmap = new WriteableBitmap(
WmtsTileMatrix.TileWidth * TileMatrix.Width,
WmtsTileMatrix.TileHeight * TileMatrix.Height,
96, 96, PixelFormats.Pbgra32, null);
CreateTiles();
InvalidateVisual();
return true;
}
private void CreateTiles()
{
var tiles = new List<BitmapTile>(TileMatrix.Width * TileMatrix.Height);
for (var y = TileMatrix.YMin; y <= TileMatrix.YMax; y++)
{
// Tile matrix bounds in pixels.
//
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)
for (var x = TileMatrix.XMin; x <= TileMatrix.XMax; x++)
{
// Set X range limits.
//
xMin = Math.Max(xMin, 0);
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
}
var tile = Tiles.FirstOrDefault(t => t.X == x && t.Y == y);
// 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);
bitmap = new WriteableBitmap(
WmtsTileMatrix.TileWidth * TileMatrix.Width,
WmtsTileMatrix.TileHeight * TileMatrix.Height,
96, 96, PixelFormats.Pbgra32, null);
CreateTiles();
InvalidateVisual();
return true;
}
private void CreateTiles()
{
var tiles = new List<BitmapTile>(TileMatrix.Width * TileMatrix.Height);
for (var y = TileMatrix.YMin; y <= TileMatrix.YMax; y++)
{
for (var x = TileMatrix.XMin; x <= TileMatrix.XMax; x++)
if (tile == null)
{
var tile = Tiles.FirstOrDefault(t => t.X == x && t.Y == y);
tile = new BitmapTile(TileMatrix.ZoomLevel, x, y, WmtsTileMatrix.MatrixWidth, WmtsTileMatrix.TileWidth, WmtsTileMatrix.TileHeight);
if (tile == null)
var equivalentTile = Tiles.FirstOrDefault(t => t.PixelBuffer != null && t.Column == tile.Column && t.Row == tile.Row);
if (equivalentTile != null)
{
tile = new BitmapTile(TileMatrix.ZoomLevel, x, y, WmtsTileMatrix.MatrixWidth, WmtsTileMatrix.TileWidth, WmtsTileMatrix.TileHeight);
var equivalentTile = Tiles.FirstOrDefault(t => t.PixelBuffer != null && t.Column == tile.Column && t.Row == tile.Row);
if (equivalentTile != null)
{
tile.IsPending = false;
tile.PixelBuffer = equivalentTile.PixelBuffer;
}
else
{
tile.Completed += OnTileCompleted;
}
tile.IsPending = false;
tile.PixelBuffer = equivalentTile.PixelBuffer;
}
if (tile.PixelBuffer != null)
else
{
CopyTile(tile);
tile.Completed += OnTileCompleted;
}
tiles.Add(tile);
}
}
Tiles = tiles;
if (tile.PixelBuffer != null)
{
CopyTile(tile);
}
tiles.Add(tile);
}
}
private void CopyTile(BitmapTile tile)
Tiles = tiles;
}
private void CopyTile(BitmapTile tile)
{
var width = WmtsTileMatrix.TileWidth;
var height = WmtsTileMatrix.TileHeight;
var x = width * (tile.X - TileMatrix.XMin);
var y = height * (tile.Y - TileMatrix.YMin);
bitmap.WritePixels(new Int32Rect(x, y, width, height), tile.PixelBuffer, 4 * WmtsTileMatrix.TileWidth, 0);
}
private void OnTileCompleted(object sender, EventArgs e)
{
var tile = (BitmapTile)sender;
tile.Completed -= OnTileCompleted;
if (tile.X >= TileMatrix.XMin && tile.X <= TileMatrix.XMax &&
tile.Y >= TileMatrix.YMin && tile.Y <= TileMatrix.YMax &&
tile.PixelBuffer != null)
{
var width = WmtsTileMatrix.TileWidth;
var height = WmtsTileMatrix.TileHeight;
var x = width * (tile.X - TileMatrix.XMin);
var y = height * (tile.Y - TileMatrix.YMin);
bitmap.WritePixels(new Int32Rect(x, y, width, height), tile.PixelBuffer, 4 * WmtsTileMatrix.TileWidth, 0);
}
private void OnTileCompleted(object sender, EventArgs e)
{
var tile = (BitmapTile)sender;
tile.Completed -= OnTileCompleted;
if (tile.X >= TileMatrix.XMin && tile.X <= TileMatrix.XMax &&
tile.Y >= TileMatrix.YMin && tile.Y <= TileMatrix.YMax &&
tile.PixelBuffer != null)
{
_ = Dispatcher.InvokeAsync(() => CopyTile(tile));
}
_ = Dispatcher.InvokeAsync(() => CopyTile(tile));
}
}
}

View file

@ -1,128 +1,127 @@
using System;
using System.Windows;
namespace MapControl
namespace MapControl;
public static class DependencyPropertyHelper
{
public static class DependencyPropertyHelper
public static DependencyProperty RegisterAttached<TValue>(
string name,
Type ownerType,
TValue defaultValue = default,
Action<FrameworkElement, TValue, TValue> changed = null,
bool inherits = false)
{
public static DependencyProperty RegisterAttached<TValue>(
string name,
Type ownerType,
TValue defaultValue = default,
Action<FrameworkElement, TValue, TValue> changed = null,
bool inherits = false)
var metadata = new FrameworkPropertyMetadata
{
var metadata = new FrameworkPropertyMetadata
{
DefaultValue = defaultValue,
Inherits = inherits
};
DefaultValue = defaultValue,
Inherits = inherits
};
if (changed != null)
if (changed != null)
{
metadata.PropertyChangedCallback = (o, e) =>
{
metadata.PropertyChangedCallback = (o, e) =>
if (o is FrameworkElement element)
{
if (o is FrameworkElement element)
{
changed(element, (TValue)e.OldValue, (TValue)e.NewValue);
}
};
}
return DependencyProperty.RegisterAttached(name, typeof(TValue), ownerType, metadata);
}
public static DependencyProperty RegisterAttached<TValue>(
string name,
Type ownerType,
TValue defaultValue,
FrameworkPropertyMetadataOptions options)
{
var metadata = new FrameworkPropertyMetadata(defaultValue, options);
return DependencyProperty.RegisterAttached(name, typeof(TValue), ownerType, metadata);
}
public static DependencyProperty Register<TOwner, TValue>(
string name,
TValue defaultValue,
FrameworkPropertyMetadataOptions options)
where TOwner : DependencyObject
{
var metadata = new FrameworkPropertyMetadata(defaultValue, options);
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
}
public static DependencyProperty Register<TOwner, TValue>(
string name,
TValue defaultValue = default,
Action<TOwner, TValue, TValue> changed = null,
Func<TOwner, TValue, TValue> coerce = null,
bool bindTwoWayByDefault = false)
where TOwner : DependencyObject
{
var metadata = new FrameworkPropertyMetadata
{
DefaultValue = defaultValue,
BindsTwoWayByDefault = bindTwoWayByDefault
changed(element, (TValue)e.OldValue, (TValue)e.NewValue);
}
};
if (changed != null)
{
metadata.PropertyChangedCallback = (o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue);
}
if (coerce != null)
{
metadata.CoerceValueCallback = (o, v) => coerce((TOwner)o, (TValue)v);
}
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
}
public static DependencyPropertyKey RegisterReadOnly<TOwner, TValue>(
string name,
TValue defaultValue = default)
where TOwner : DependencyObject
return DependencyProperty.RegisterAttached(name, typeof(TValue), ownerType, metadata);
}
public static DependencyProperty RegisterAttached<TValue>(
string name,
Type ownerType,
TValue defaultValue,
FrameworkPropertyMetadataOptions options)
{
var metadata = new FrameworkPropertyMetadata(defaultValue, options);
return DependencyProperty.RegisterAttached(name, typeof(TValue), ownerType, metadata);
}
public static DependencyProperty Register<TOwner, TValue>(
string name,
TValue defaultValue,
FrameworkPropertyMetadataOptions options)
where TOwner : DependencyObject
{
var metadata = new FrameworkPropertyMetadata(defaultValue, options);
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
}
public static DependencyProperty Register<TOwner, TValue>(
string name,
TValue defaultValue = default,
Action<TOwner, TValue, TValue> changed = null,
Func<TOwner, TValue, TValue> coerce = null,
bool bindTwoWayByDefault = false)
where TOwner : DependencyObject
{
var metadata = new FrameworkPropertyMetadata
{
return DependencyProperty.RegisterReadOnly(name, typeof(TValue), typeof(TOwner), new PropertyMetadata(defaultValue));
DefaultValue = defaultValue,
BindsTwoWayByDefault = bindTwoWayByDefault
};
if (changed != null)
{
metadata.PropertyChangedCallback = (o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue);
}
public static DependencyProperty AddOwner<TOwner, TValue>(
DependencyProperty source) where TOwner : DependencyObject
if (coerce != null)
{
return source.AddOwner(typeof(TOwner));
metadata.CoerceValueCallback = (o, v) => coerce((TOwner)o, (TValue)v);
}
public static DependencyProperty AddOwner<TOwner, TValue>(
DependencyProperty source,
TValue defaultValue) where TOwner : DependencyObject
{
return source.AddOwner(typeof(TOwner), new FrameworkPropertyMetadata(defaultValue));
}
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
}
public static DependencyProperty AddOwner<TOwner, TValue>(
DependencyProperty source,
Action<TOwner, TValue, TValue> changed) where TOwner : DependencyObject
{
return source.AddOwner(typeof(TOwner), new FrameworkPropertyMetadata(
(o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue)));
}
public static DependencyPropertyKey RegisterReadOnly<TOwner, TValue>(
string name,
TValue defaultValue = default)
where TOwner : DependencyObject
{
return DependencyProperty.RegisterReadOnly(name, typeof(TValue), typeof(TOwner), new PropertyMetadata(defaultValue));
}
public static DependencyProperty AddOwner<TOwner, TValue>(
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
DependencyProperty source) where TOwner : DependencyObject
{
return AddOwner<TOwner, TValue>(source);
}
public static DependencyProperty AddOwner<TOwner, TValue>(
DependencyProperty source) where TOwner : DependencyObject
{
return source.AddOwner(typeof(TOwner));
}
public static DependencyProperty AddOwner<TOwner, TValue>(
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
DependencyProperty source,
Action<TOwner, TValue, TValue> changed) where TOwner : DependencyObject
{
return AddOwner(source, changed);
}
public static DependencyProperty AddOwner<TOwner, TValue>(
DependencyProperty source,
TValue defaultValue) where TOwner : DependencyObject
{
return source.AddOwner(typeof(TOwner), new FrameworkPropertyMetadata(defaultValue));
}
public static DependencyProperty AddOwner<TOwner, TValue>(
DependencyProperty source,
Action<TOwner, TValue, TValue> changed) where TOwner : DependencyObject
{
return source.AddOwner(typeof(TOwner), new FrameworkPropertyMetadata(
(o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue)));
}
public static DependencyProperty AddOwner<TOwner, TValue>(
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
DependencyProperty source) where TOwner : DependencyObject
{
return AddOwner<TOwner, TValue>(source);
}
public static DependencyProperty AddOwner<TOwner, TValue>(
string _, // for compatibility with WinUI/UWP DependencyPropertyHelper
DependencyProperty source,
Action<TOwner, TValue, TValue> changed) where TOwner : DependencyObject
{
return AddOwner(source, changed);
}
}

View file

@ -4,75 +4,74 @@ using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
namespace MapControl
namespace MapControl;
public class DrawingTile : Tile
{
public class DrawingTile : Tile
public DrawingTile(int zoomLevel, int x, int y, int columnCount)
: base(zoomLevel, x, y, columnCount)
{
public DrawingTile(int zoomLevel, int x, int y, int columnCount)
: base(zoomLevel, x, y, columnCount)
Drawing.Children.Add(ImageDrawing);
}
public DrawingGroup Drawing { get; } = new DrawingGroup();
public ImageDrawing ImageDrawing { get; } = new ImageDrawing();
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
{
var image = await loadImageFunc().ConfigureAwait(false);
void SetImageSource()
{
Drawing.Children.Add(ImageDrawing);
}
ImageDrawing.ImageSource = image;
public DrawingGroup Drawing { get; } = new DrawingGroup();
public ImageDrawing ImageDrawing { get; } = new ImageDrawing();
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
{
var image = await loadImageFunc().ConfigureAwait(false);
void SetImageSource()
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
{
ImageDrawing.ImageSource = image;
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
if (image is BitmapSource bitmap && !bitmap.IsFrozen && bitmap.IsDownloading)
{
if (image is BitmapSource bitmap && !bitmap.IsFrozen && bitmap.IsDownloading)
{
bitmap.DownloadCompleted += BitmapDownloadCompleted;
bitmap.DownloadFailed += BitmapDownloadFailed;
}
else
{
BeginFadeInAnimation();
}
bitmap.DownloadCompleted += BitmapDownloadCompleted;
bitmap.DownloadFailed += BitmapDownloadFailed;
}
else
{
BeginFadeInAnimation();
}
}
await Drawing.Dispatcher.InvokeAsync(SetImageSource);
}
private void BeginFadeInAnimation()
await Drawing.Dispatcher.InvokeAsync(SetImageSource);
}
private void BeginFadeInAnimation()
{
var fadeInAnimation = new DoubleAnimation
{
var fadeInAnimation = new DoubleAnimation
{
From = 0d,
Duration = MapBase.ImageFadeDuration,
FillBehavior = FillBehavior.Stop
};
From = 0d,
Duration = MapBase.ImageFadeDuration,
FillBehavior = FillBehavior.Stop
};
Drawing.BeginAnimation(DrawingGroup.OpacityProperty, fadeInAnimation);
}
Drawing.BeginAnimation(DrawingGroup.OpacityProperty, fadeInAnimation);
}
private void BitmapDownloadCompleted(object sender, EventArgs e)
{
var bitmap = (BitmapSource)sender;
private void BitmapDownloadCompleted(object sender, EventArgs e)
{
var bitmap = (BitmapSource)sender;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
BeginFadeInAnimation();
}
BeginFadeInAnimation();
}
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)
{
var bitmap = (BitmapSource)sender;
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)
{
var bitmap = (BitmapSource)sender;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
ImageDrawing.ImageSource = null;
}
ImageDrawing.ImageSource = null;
}
}

View file

@ -4,113 +4,112 @@ using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace MapControl
namespace MapControl;
public class DrawingTileMatrixLayer(WmtsTileMatrix wmtsTileMatrix, int zoomLevel) : UIElement
{
public class DrawingTileMatrixLayer(WmtsTileMatrix wmtsTileMatrix, int zoomLevel) : UIElement
private readonly MatrixTransform transform = new MatrixTransform();
public WmtsTileMatrix WmtsTileMatrix => wmtsTileMatrix;
public TileMatrix TileMatrix { get; private set; } = new TileMatrix(zoomLevel, 1, 1, 0, 0);
public IEnumerable<DrawingTile> Tiles { get; private set; } = [];
protected override void OnRender(DrawingContext drawingContext)
{
private readonly MatrixTransform transform = new MatrixTransform();
drawingContext.PushTransform(transform);
public WmtsTileMatrix WmtsTileMatrix => wmtsTileMatrix;
public TileMatrix TileMatrix { get; private set; } = new TileMatrix(zoomLevel, 1, 1, 0, 0);
public IEnumerable<DrawingTile> Tiles { get; private set; } = [];
protected override void OnRender(DrawingContext drawingContext)
foreach (var tile in Tiles)
{
drawingContext.PushTransform(transform);
foreach (var tile in Tiles)
{
drawingContext.DrawDrawing(tile.Drawing);
}
}
public void UpdateRenderTransform(ViewTransform viewTransform)
{
// Tile matrix origin in pixels.
//
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
transform.Matrix = viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
}
public bool UpdateTiles(ViewTransform viewTransform, double viewWidth, double viewHeight)
{
// Tile matrix bounds in pixels.
//
var bounds = viewTransform.GetTileMatrixBounds(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, viewWidth, viewHeight);
// Tile X and Y bounds.
//
var xMin = (int)Math.Floor(bounds.X / WmtsTileMatrix.TileWidth);
var yMin = (int)Math.Floor(bounds.Y / WmtsTileMatrix.TileHeight);
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / WmtsTileMatrix.TileWidth);
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / WmtsTileMatrix.TileHeight);
if (!WmtsTileMatrix.HasFullHorizontalCoverage)
{
// Set X range limits.
//
xMin = Math.Max(xMin, 0);
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
}
// Set Y range limits.
//
yMin = Math.Max(yMin, 0);
yMax = Math.Min(Math.Max(yMax, 0), WmtsTileMatrix.MatrixHeight - 1);
if (TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
{
// No change of the TileMatrix and the Tiles collection.
//
return false;
}
TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax);
CreateTiles();
InvalidateVisual();
return true;
}
private void CreateTiles()
{
var tiles = new List<DrawingTile>(TileMatrix.Width * TileMatrix.Height);
for (var y = TileMatrix.YMin; y <= TileMatrix.YMax; y++)
{
for (var x = TileMatrix.XMin; x <= TileMatrix.XMax; x++)
{
var tile = Tiles.FirstOrDefault(t => t.X == x && t.Y == y);
if (tile == null)
{
tile = new DrawingTile(TileMatrix.ZoomLevel, x, y, WmtsTileMatrix.MatrixWidth);
var equivalentTile = Tiles.FirstOrDefault(t => t.ImageDrawing.ImageSource != null && t.Column == tile.Column && t.Row == tile.Row);
if (equivalentTile != null)
{
tile.IsPending = false;
tile.ImageDrawing.ImageSource = equivalentTile.ImageDrawing.ImageSource; // no Opacity animation
}
}
tile.ImageDrawing.Rect = new Rect(
WmtsTileMatrix.TileWidth * (x - TileMatrix.XMin),
WmtsTileMatrix.TileHeight * (y - TileMatrix.YMin),
WmtsTileMatrix.TileWidth,
WmtsTileMatrix.TileHeight);
tiles.Add(tile);
}
}
Tiles = tiles;
drawingContext.DrawDrawing(tile.Drawing);
}
}
public void UpdateRenderTransform(ViewTransform viewTransform)
{
// Tile matrix origin in pixels.
//
var tileMatrixOrigin = new Point(WmtsTileMatrix.TileWidth * TileMatrix.XMin, WmtsTileMatrix.TileHeight * TileMatrix.YMin);
transform.Matrix = viewTransform.GetTileLayerTransform(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, tileMatrixOrigin);
}
public bool UpdateTiles(ViewTransform viewTransform, double viewWidth, double viewHeight)
{
// Tile matrix bounds in pixels.
//
var bounds = viewTransform.GetTileMatrixBounds(WmtsTileMatrix.Scale, WmtsTileMatrix.TopLeft, viewWidth, viewHeight);
// Tile X and Y bounds.
//
var xMin = (int)Math.Floor(bounds.X / WmtsTileMatrix.TileWidth);
var yMin = (int)Math.Floor(bounds.Y / WmtsTileMatrix.TileHeight);
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / WmtsTileMatrix.TileWidth);
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / WmtsTileMatrix.TileHeight);
if (!WmtsTileMatrix.HasFullHorizontalCoverage)
{
// Set X range limits.
//
xMin = Math.Max(xMin, 0);
xMax = Math.Min(Math.Max(xMax, 0), WmtsTileMatrix.MatrixWidth - 1);
}
// Set Y range limits.
//
yMin = Math.Max(yMin, 0);
yMax = Math.Min(Math.Max(yMax, 0), WmtsTileMatrix.MatrixHeight - 1);
if (TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
{
// No change of the TileMatrix and the Tiles collection.
//
return false;
}
TileMatrix = new TileMatrix(TileMatrix.ZoomLevel, xMin, yMin, xMax, yMax);
CreateTiles();
InvalidateVisual();
return true;
}
private void CreateTiles()
{
var tiles = new List<DrawingTile>(TileMatrix.Width * TileMatrix.Height);
for (var y = TileMatrix.YMin; y <= TileMatrix.YMax; y++)
{
for (var x = TileMatrix.XMin; x <= TileMatrix.XMax; x++)
{
var tile = Tiles.FirstOrDefault(t => t.X == x && t.Y == y);
if (tile == null)
{
tile = new DrawingTile(TileMatrix.ZoomLevel, x, y, WmtsTileMatrix.MatrixWidth);
var equivalentTile = Tiles.FirstOrDefault(t => t.ImageDrawing.ImageSource != null && t.Column == tile.Column && t.Row == tile.Row);
if (equivalentTile != null)
{
tile.IsPending = false;
tile.ImageDrawing.ImageSource = equivalentTile.ImageDrawing.ImageSource; // no Opacity animation
}
}
tile.ImageDrawing.Rect = new Rect(
WmtsTileMatrix.TileWidth * (x - TileMatrix.XMin),
WmtsTileMatrix.TileHeight * (y - TileMatrix.YMin),
WmtsTileMatrix.TileWidth,
WmtsTileMatrix.TileHeight);
tiles.Add(tile);
}
}
Tiles = tiles;
}
}

View file

@ -5,113 +5,112 @@ using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace MapControl
namespace MapControl;
public static partial class GeoImage
{
public static partial class GeoImage
private static Task<GeoBitmap> LoadGeoTiff(string sourcePath)
{
private static Task<GeoBitmap> LoadGeoTiff(string sourcePath)
return Task.Run(() =>
{
return Task.Run(() =>
BitmapSource bitmap;
Matrix transform;
MapProjection projection = null;
using (var stream = File.OpenRead(sourcePath))
{
BitmapSource bitmap;
Matrix transform;
MapProjection projection = null;
bitmap = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
using (var stream = File.OpenRead(sourcePath))
{
bitmap = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
var metadata = (BitmapMetadata)bitmap.Metadata;
var metadata = (BitmapMetadata)bitmap.Metadata;
if (metadata.GetQuery(QueryString(ModelPixelScaleTag)) is double[] pixelScale &&
pixelScale.Length == 3 &&
metadata.GetQuery(QueryString(ModelTiePointTag)) is double[] tiePoint &&
tiePoint.Length >= 6)
{
transform = new Matrix(pixelScale[0], 0d, 0d, -pixelScale[1], tiePoint[3], tiePoint[4]);
}
else if (metadata.GetQuery(QueryString(ModelTransformationTag)) is double[] transformValues &&
transformValues.Length == 16)
{
transform = new Matrix(transformValues[0], transformValues[1],
transformValues[4], transformValues[5],
transformValues[3], transformValues[7]);
}
else
{
throw new ArgumentException("No coordinate transformation found.");
}
if (metadata.GetQuery(QueryString(ModelPixelScaleTag)) is double[] pixelScale &&
pixelScale.Length == 3 &&
metadata.GetQuery(QueryString(ModelTiePointTag)) is double[] tiePoint &&
tiePoint.Length >= 6)
{
transform = new Matrix(pixelScale[0], 0d, 0d, -pixelScale[1], tiePoint[3], tiePoint[4]);
}
else if (metadata.GetQuery(QueryString(ModelTransformationTag)) is double[] transformValues &&
transformValues.Length == 16)
{
transform = new Matrix(transformValues[0], transformValues[1],
transformValues[4], transformValues[5],
transformValues[3], transformValues[7]);
}
else
{
throw new ArgumentException("No coordinate transformation found.");
}
if (metadata.GetQuery(QueryString(GeoKeyDirectoryTag)) is short[] geoKeyDirectory)
{
projection = GetProjection(geoKeyDirectory);
}
if (metadata.GetQuery(QueryString(GeoKeyDirectoryTag)) is short[] geoKeyDirectory)
{
projection = GetProjection(geoKeyDirectory);
}
if (metadata.GetQuery(QueryString(NoDataTag)) is string noData &&
int.TryParse(noData, out int noDataValue))
{
bitmap = ConvertTransparentPixel(bitmap, noDataValue);
}
if (metadata.GetQuery(QueryString(NoDataTag)) is string noData &&
int.TryParse(noData, out int noDataValue))
{
bitmap = ConvertTransparentPixel(bitmap, noDataValue);
}
return new GeoBitmap(bitmap, transform, projection);
});
}
return new GeoBitmap(bitmap, transform, projection);
});
private static BitmapSource ConvertTransparentPixel(BitmapSource source, int transparentPixel)
{
BitmapPalette sourcePalette = null;
var targetFormat = source.Format;
if (source.Format == PixelFormats.Indexed8 ||
source.Format == PixelFormats.Indexed4 ||
source.Format == PixelFormats.Indexed2 ||
source.Format == PixelFormats.Indexed1)
{
sourcePalette = source.Palette;
}
else if (source.Format == PixelFormats.Gray8)
{
sourcePalette = BitmapPalettes.Gray256;
targetFormat = PixelFormats.Indexed8;
}
else if (source.Format == PixelFormats.Gray4)
{
sourcePalette = BitmapPalettes.Gray16;
targetFormat = PixelFormats.Indexed4;
}
else if (source.Format == PixelFormats.Gray2)
{
sourcePalette = BitmapPalettes.Gray4;
targetFormat = PixelFormats.Indexed2;
}
else if (source.Format == PixelFormats.BlackWhite)
{
sourcePalette = BitmapPalettes.BlackAndWhite;
targetFormat = PixelFormats.Indexed1;
}
private static BitmapSource ConvertTransparentPixel(BitmapSource source, int transparentPixel)
if (sourcePalette == null || transparentPixel >= sourcePalette.Colors.Count)
{
BitmapPalette sourcePalette = null;
var targetFormat = source.Format;
if (source.Format == PixelFormats.Indexed8 ||
source.Format == PixelFormats.Indexed4 ||
source.Format == PixelFormats.Indexed2 ||
source.Format == PixelFormats.Indexed1)
{
sourcePalette = source.Palette;
}
else if (source.Format == PixelFormats.Gray8)
{
sourcePalette = BitmapPalettes.Gray256;
targetFormat = PixelFormats.Indexed8;
}
else if (source.Format == PixelFormats.Gray4)
{
sourcePalette = BitmapPalettes.Gray16;
targetFormat = PixelFormats.Indexed4;
}
else if (source.Format == PixelFormats.Gray2)
{
sourcePalette = BitmapPalettes.Gray4;
targetFormat = PixelFormats.Indexed2;
}
else if (source.Format == PixelFormats.BlackWhite)
{
sourcePalette = BitmapPalettes.BlackAndWhite;
targetFormat = PixelFormats.Indexed1;
}
if (sourcePalette == null || transparentPixel >= sourcePalette.Colors.Count)
{
return source;
}
var colors = sourcePalette.Colors.ToList();
colors[transparentPixel] = Colors.Transparent;
var stride = (source.PixelWidth * source.Format.BitsPerPixel + 7) / 8;
var buffer = new byte[stride * source.PixelHeight];
source.CopyPixels(buffer, stride, 0);
var target = BitmapSource.Create(
source.PixelWidth, source.PixelHeight, source.DpiX, source.DpiY,
targetFormat, new BitmapPalette(colors), buffer, stride);
target.Freeze();
return target;
return source;
}
var colors = sourcePalette.Colors.ToList();
colors[transparentPixel] = Colors.Transparent;
var stride = (source.PixelWidth * source.Format.BitsPerPixel + 7) / 8;
var buffer = new byte[stride * source.PixelHeight];
source.CopyPixels(buffer, stride, 0);
var target = BitmapSource.Create(
source.PixelWidth, source.PixelHeight, source.DpiX, source.DpiY,
targetFormat, new BitmapPalette(colors), buffer, stride);
target.Freeze();
return target;
}
}

View file

@ -6,92 +6,91 @@ using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace MapControl
namespace MapControl;
public static partial class ImageLoader
{
public static partial class ImageLoader
public static ImageSource LoadResourceImage(Uri uri)
{
public static ImageSource LoadResourceImage(Uri uri)
return new BitmapImage(uri);
}
public static ImageSource LoadImage(Stream stream)
{
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
return image;
}
public static ImageSource LoadImage(string path)
{
ImageSource image = null;
if (File.Exists(path))
{
return new BitmapImage(uri);
using var stream = File.OpenRead(path);
image = LoadImage(stream);
}
public static ImageSource LoadImage(Stream stream)
return image;
}
public static Task<ImageSource> LoadImageAsync(Stream stream)
{
return Thread.CurrentThread.IsThreadPoolThread ?
Task.FromResult(LoadImage(stream)) :
Task.Run(() => LoadImage(stream));
}
public static Task<ImageSource> LoadImageAsync(string path)
{
return Thread.CurrentThread.IsThreadPoolThread ?
Task.FromResult(LoadImage(path)) :
Task.Run(() => LoadImage(path));
}
internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
{
WriteableBitmap mergedBitmap = null;
var p1 = 0d;
var p2 = 0d;
var images = await Task.WhenAll(
LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
if (images.Length == 2 &&
images[0] is BitmapSource bitmap1 &&
images[1] is BitmapSource bitmap2 &&
bitmap1.PixelHeight == bitmap2.PixelHeight &&
bitmap1.Format == bitmap2.Format &&
bitmap1.Format.BitsPerPixel % 8 == 0)
{
var image = new BitmapImage();
var format = bitmap1.Format;
var height = bitmap1.PixelHeight;
var width1 = bitmap1.PixelWidth;
var width2 = bitmap2.PixelWidth;
var stride1 = width1 * format.BitsPerPixel / 8;
var stride2 = width2 * format.BitsPerPixel / 8;
var buffer1 = new byte[stride1 * height];
var buffer2 = new byte[stride2 * height];
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
bitmap1.CopyPixels(buffer1, stride1, 0);
bitmap2.CopyPixels(buffer2, stride2, 0);
return image;
mergedBitmap = new WriteableBitmap(width1 + width2, height, 96, 96, format, null);
mergedBitmap.WritePixels(new Int32Rect(0, 0, width1, height), buffer1, stride1, 0);
mergedBitmap.WritePixels(new Int32Rect(width1, 0, width2, height), buffer2, stride2, 0);
mergedBitmap.Freeze();
}
public static ImageSource LoadImage(string path)
{
ImageSource image = null;
if (File.Exists(path))
{
using var stream = File.OpenRead(path);
image = LoadImage(stream);
}
return image;
}
public static Task<ImageSource> LoadImageAsync(Stream stream)
{
return Thread.CurrentThread.IsThreadPoolThread ?
Task.FromResult(LoadImage(stream)) :
Task.Run(() => LoadImage(stream));
}
public static Task<ImageSource> LoadImageAsync(string path)
{
return Thread.CurrentThread.IsThreadPoolThread ?
Task.FromResult(LoadImage(path)) :
Task.Run(() => LoadImage(path));
}
internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
{
WriteableBitmap mergedBitmap = null;
var p1 = 0d;
var p2 = 0d;
var images = await Task.WhenAll(
LoadImageAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
LoadImageAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
if (images.Length == 2 &&
images[0] is BitmapSource bitmap1 &&
images[1] is BitmapSource bitmap2 &&
bitmap1.PixelHeight == bitmap2.PixelHeight &&
bitmap1.Format == bitmap2.Format &&
bitmap1.Format.BitsPerPixel % 8 == 0)
{
var format = bitmap1.Format;
var height = bitmap1.PixelHeight;
var width1 = bitmap1.PixelWidth;
var width2 = bitmap2.PixelWidth;
var stride1 = width1 * format.BitsPerPixel / 8;
var stride2 = width2 * format.BitsPerPixel / 8;
var buffer1 = new byte[stride1 * height];
var buffer2 = new byte[stride2 * height];
bitmap1.CopyPixels(buffer1, stride1, 0);
bitmap2.CopyPixels(buffer2, stride2, 0);
mergedBitmap = new WriteableBitmap(width1 + width2, height, 96, 96, format, null);
mergedBitmap.WritePixels(new Int32Rect(0, 0, width1, height), buffer1, stride1, 0);
mergedBitmap.WritePixels(new Int32Rect(width1, 0, width2, height), buffer2, stride2, 0);
mergedBitmap.Freeze();
}
return mergedBitmap;
}
return mergedBitmap;
}
}

View file

@ -6,68 +6,67 @@ using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
namespace MapControl
namespace MapControl;
public class ImageTile(int zoomLevel, int x, int y, int columnCount)
: Tile(zoomLevel, x, y, columnCount)
{
public class ImageTile(int zoomLevel, int x, int y, int columnCount)
: Tile(zoomLevel, x, y, columnCount)
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
{
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
var image = await loadImageFunc().ConfigureAwait(false);
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
void SetImageSource()
{
var image = await loadImageFunc().ConfigureAwait(false);
Image.Source = image;
void SetImageSource()
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
{
Image.Source = image;
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
if (image is BitmapSource bitmap && !bitmap.IsFrozen && bitmap.IsDownloading)
{
if (image is BitmapSource bitmap && !bitmap.IsFrozen && bitmap.IsDownloading)
{
bitmap.DownloadCompleted += BitmapDownloadCompleted;
bitmap.DownloadFailed += BitmapDownloadFailed;
}
else
{
BeginFadeInAnimation();
}
bitmap.DownloadCompleted += BitmapDownloadCompleted;
bitmap.DownloadFailed += BitmapDownloadFailed;
}
else
{
BeginFadeInAnimation();
}
}
await Image.Dispatcher.InvokeAsync(SetImageSource);
}
private void BeginFadeInAnimation()
await Image.Dispatcher.InvokeAsync(SetImageSource);
}
private void BeginFadeInAnimation()
{
var fadeInAnimation = new DoubleAnimation
{
var fadeInAnimation = new DoubleAnimation
{
From = 0d,
Duration = MapBase.ImageFadeDuration,
FillBehavior = FillBehavior.Stop
};
From = 0d,
Duration = MapBase.ImageFadeDuration,
FillBehavior = FillBehavior.Stop
};
Image.BeginAnimation(UIElement.OpacityProperty, fadeInAnimation);
}
Image.BeginAnimation(UIElement.OpacityProperty, fadeInAnimation);
}
private void BitmapDownloadCompleted(object sender, EventArgs e)
{
var bitmap = (BitmapSource)sender;
private void BitmapDownloadCompleted(object sender, EventArgs e)
{
var bitmap = (BitmapSource)sender;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
BeginFadeInAnimation();
}
BeginFadeInAnimation();
}
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)
{
var bitmap = (BitmapSource)sender;
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)
{
var bitmap = (BitmapSource)sender;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
Image.Source = null;
}
Image.Source = null;
}
}

View file

@ -2,38 +2,37 @@
using System.Windows;
using System.Windows.Media.Animation;
namespace MapControl
namespace MapControl;
public class LocationAnimation : AnimationTimeline
{
public class LocationAnimation : AnimationTimeline
public override Type TargetPropertyType => typeof(Location);
public Location To { get; set; }
public IEasingFunction EasingFunction { get; set; }
protected override Freezable CreateInstanceCore()
{
public override Type TargetPropertyType => typeof(Location);
public Location To { get; set; }
public IEasingFunction EasingFunction { get; set; }
protected override Freezable CreateInstanceCore()
return new LocationAnimation
{
return new LocationAnimation
{
To = To,
EasingFunction = EasingFunction
};
To = To,
EasingFunction = EasingFunction
};
}
public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock)
{
var from = (Location)defaultOriginValue;
var progress = animationClock.CurrentProgress ?? 1d;
if (EasingFunction != null)
{
progress = EasingFunction.Ease(progress);
}
public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock)
{
var from = (Location)defaultOriginValue;
var progress = animationClock.CurrentProgress ?? 1d;
if (EasingFunction != null)
{
progress = EasingFunction.Ease(progress);
}
return new Location(
(1d - progress) * from.Latitude + progress * To.Latitude,
(1d - progress) * from.Longitude + progress * To.Longitude);
}
return new Location(
(1d - progress) * from.Latitude + progress * To.Latitude,
(1d - progress) * from.Longitude + progress * To.Longitude);
}
}

View file

@ -1,93 +1,92 @@
using System.Windows;
using System.Windows.Input;
namespace MapControl
namespace MapControl;
public partial class Map
{
public partial class Map
static Map()
{
static Map()
IsManipulationEnabledProperty.OverrideMetadata(typeof(Map), new FrameworkPropertyMetadata(true));
}
public static readonly DependencyProperty ManipulationModeProperty =
DependencyPropertyHelper.Register<Map, ManipulationModes>(nameof(ManipulationMode), ManipulationModes.Translate | ManipulationModes.Scale);
/// <summary>
/// Gets or sets a value that specifies how the map control handles manipulations.
/// </summary>
public ManipulationModes ManipulationMode
{
get => (ManipulationModes)GetValue(ManipulationModeProperty);
set => SetValue(ManipulationModeProperty, value);
}
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
base.OnMouseWheel(e);
// Standard mouse wheel delta value is 120.
//
OnMouseWheel(e.GetPosition(this), e.Delta / 120d);
}
private Point? mousePosition;
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnPreviewMouseLeftButtonDown(e);
if (Keyboard.Modifiers == ModifierKeys.None)
{
IsManipulationEnabledProperty.OverrideMetadata(typeof(Map), new FrameworkPropertyMetadata(true));
}
public static readonly DependencyProperty ManipulationModeProperty =
DependencyPropertyHelper.Register<Map, ManipulationModes>(nameof(ManipulationMode), ManipulationModes.Translate | ManipulationModes.Scale);
/// <summary>
/// Gets or sets a value that specifies how the map control handles manipulations.
/// </summary>
public ManipulationModes ManipulationMode
{
get => (ManipulationModes)GetValue(ManipulationModeProperty);
set => SetValue(ManipulationModeProperty, value);
}
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
base.OnMouseWheel(e);
// Standard mouse wheel delta value is 120.
// Do not call CaptureMouse here because it avoids MapItem selection.
//
OnMouseWheel(e.GetPosition(this), e.Delta / 120d);
}
private Point? mousePosition;
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnPreviewMouseLeftButtonDown(e);
if (Keyboard.Modifiers == ModifierKeys.None)
{
// Do not call CaptureMouse here because it avoids MapItem selection.
//
mousePosition = e.GetPosition(this);
}
}
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnPreviewMouseLeftButtonUp(e);
if (mousePosition.HasValue)
{
mousePosition = null;
ReleaseMouseCapture();
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (mousePosition.HasValue)
{
if (!IsMouseCaptured)
{
CaptureMouse();
}
var p = e.GetPosition(this);
TranslateMap(new Point(p.X - mousePosition.Value.X, p.Y - mousePosition.Value.Y));
mousePosition = p;
}
}
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
{
Manipulation.SetManipulationMode(this, ManipulationMode);
base.OnManipulationStarted(e);
}
protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
{
base.OnManipulationDelta(e);
TransformMap(e.ManipulationOrigin,
(Point)e.DeltaManipulation.Translation,
e.DeltaManipulation.Rotation,
e.DeltaManipulation.Scale.LengthSquared / 2d);
mousePosition = e.GetPosition(this);
}
}
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnPreviewMouseLeftButtonUp(e);
if (mousePosition.HasValue)
{
mousePosition = null;
ReleaseMouseCapture();
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (mousePosition.HasValue)
{
if (!IsMouseCaptured)
{
CaptureMouse();
}
var p = e.GetPosition(this);
TranslateMap(new Point(p.X - mousePosition.Value.X, p.Y - mousePosition.Value.Y));
mousePosition = p;
}
}
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
{
Manipulation.SetManipulationMode(this, ManipulationMode);
base.OnManipulationStarted(e);
}
protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
{
base.OnManipulationDelta(e);
TransformMap(e.ManipulationOrigin,
(Point)e.DeltaManipulation.Translation,
e.DeltaManipulation.Rotation,
e.DeltaManipulation.Scale.LengthSquared / 2d);
}
}

View file

@ -3,278 +3,277 @@ using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace MapControl
namespace MapControl;
public partial class MapBase
{
public partial class MapBase
public static readonly DependencyProperty ForegroundProperty =
DependencyPropertyHelper.AddOwner<MapBase, Brush>(TextElement.ForegroundProperty, Brushes.Black);
public static readonly DependencyProperty AnimationEasingFunctionProperty =
DependencyPropertyHelper.Register<MapBase, IEasingFunction>(nameof(AnimationEasingFunction),
new QuadraticEase { EasingMode = EasingMode.EaseOut });
public static readonly DependencyProperty CenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(Center), new Location(0d, 0d),
(map, oldValue, newValue) => map.CenterPropertyChanged(newValue),
(map, value) => map.CoerceCenterProperty(value),
true);
public static readonly DependencyProperty TargetCenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(0d, 0d),
(map, oldValue, newValue) => map.TargetCenterPropertyChanged(newValue),
(map, value) => map.CoerceCenterProperty(value),
true);
public static readonly DependencyProperty MinZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d,
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceMinZoomLevelProperty(value));
public static readonly DependencyProperty MaxZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d,
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceMaxZoomLevelProperty(value));
public static readonly DependencyProperty ZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d,
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceZoomLevelProperty(value),
true);
public static readonly DependencyProperty TargetZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d,
(map, oldValue, newValue) => map.TargetZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceZoomLevelProperty(value),
true);
public static readonly DependencyProperty HeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d,
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue),
(map, value) => map.CoerceHeadingProperty(value),
true);
public static readonly DependencyProperty TargetHeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d,
(map, oldValue, newValue) => map.TargetHeadingPropertyChanged(newValue),
(map, value) => map.CoerceHeadingProperty(value),
true);
private static readonly DependencyPropertyKey ViewScalePropertyKey =
DependencyPropertyHelper.RegisterReadOnly<MapBase, double>(nameof(ViewScale));
public static readonly DependencyProperty ViewScaleProperty = ViewScalePropertyKey.DependencyProperty;
private LocationAnimation centerAnimation;
private DoubleAnimation zoomLevelAnimation;
private DoubleAnimation headingAnimation;
static MapBase()
{
public static readonly DependencyProperty ForegroundProperty =
DependencyPropertyHelper.AddOwner<MapBase, Brush>(TextElement.ForegroundProperty, Brushes.Black);
BackgroundProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(Brushes.White));
ClipToBoundsProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(true));
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(typeof(MapBase)));
}
public static readonly DependencyProperty AnimationEasingFunctionProperty =
DependencyPropertyHelper.Register<MapBase, IEasingFunction>(nameof(AnimationEasingFunction),
new QuadraticEase { EasingMode = EasingMode.EaseOut });
/// <summary>
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
/// The default value is a QuadraticEase with EasingMode.EaseOut.
/// </summary>
public IEasingFunction AnimationEasingFunction
{
get => (IEasingFunction)GetValue(AnimationEasingFunctionProperty);
set => SetValue(AnimationEasingFunctionProperty, value);
}
public static readonly DependencyProperty CenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(Center), new Location(0d, 0d),
(map, oldValue, newValue) => map.CenterPropertyChanged(newValue),
(map, value) => map.CoerceCenterProperty(value),
true);
/// <summary>
/// Gets the scaling factor from projected map coordinates to view coordinates,
/// as pixels per meter.
/// </summary>
public double ViewScale
{
get => (double)GetValue(ViewScaleProperty);
private set => SetValue(ViewScalePropertyKey, value);
}
public static readonly DependencyProperty TargetCenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(0d, 0d),
(map, oldValue, newValue) => map.TargetCenterPropertyChanged(newValue),
(map, value) => map.CoerceCenterProperty(value),
true);
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
public static readonly DependencyProperty MinZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d,
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceMinZoomLevelProperty(value));
ResetTransformCenter();
UpdateTransform();
}
public static readonly DependencyProperty MaxZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d,
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceMaxZoomLevelProperty(value));
public static readonly DependencyProperty ZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d,
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceZoomLevelProperty(value),
true);
public static readonly DependencyProperty TargetZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d,
(map, oldValue, newValue) => map.TargetZoomLevelPropertyChanged(newValue),
(map, value) => map.CoerceZoomLevelProperty(value),
true);
public static readonly DependencyProperty HeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d,
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue),
(map, value) => map.CoerceHeadingProperty(value),
true);
public static readonly DependencyProperty TargetHeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d,
(map, oldValue, newValue) => map.TargetHeadingPropertyChanged(newValue),
(map, value) => map.CoerceHeadingProperty(value),
true);
private static readonly DependencyPropertyKey ViewScalePropertyKey =
DependencyPropertyHelper.RegisterReadOnly<MapBase, double>(nameof(ViewScale));
public static readonly DependencyProperty ViewScaleProperty = ViewScalePropertyKey.DependencyProperty;
private LocationAnimation centerAnimation;
private DoubleAnimation zoomLevelAnimation;
private DoubleAnimation headingAnimation;
static MapBase()
private void CenterPropertyChanged(Location center)
{
if (!internalPropertyChange)
{
BackgroundProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(Brushes.White));
ClipToBoundsProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(true));
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(typeof(MapBase)));
}
/// <summary>
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
/// The default value is a QuadraticEase with EasingMode.EaseOut.
/// </summary>
public IEasingFunction AnimationEasingFunction
{
get => (IEasingFunction)GetValue(AnimationEasingFunctionProperty);
set => SetValue(AnimationEasingFunctionProperty, value);
}
/// <summary>
/// Gets the scaling factor from projected map coordinates to view coordinates,
/// as pixels per meter.
/// </summary>
public double ViewScale
{
get => (double)GetValue(ViewScaleProperty);
private set => SetValue(ViewScalePropertyKey, value);
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
ResetTransformCenter();
UpdateTransform();
}
private void CenterPropertyChanged(Location center)
{
if (!internalPropertyChange)
if (centerAnimation == null)
{
UpdateTransform();
if (centerAnimation == null)
{
SetValueInternal(TargetCenterProperty, center);
}
SetValueInternal(TargetCenterProperty, center);
}
}
}
private void TargetCenterPropertyChanged(Location targetCenter)
private void TargetCenterPropertyChanged(Location targetCenter)
{
if (!internalPropertyChange && !targetCenter.Equals(Center))
{
if (!internalPropertyChange && !targetCenter.Equals(Center))
{
ResetTransformCenter();
ResetTransformCenter();
if (centerAnimation != null)
{
centerAnimation.Completed -= CenterAnimationCompleted;
}
centerAnimation = new LocationAnimation
{
To = new Location(targetCenter.Latitude, NearestLongitude(targetCenter.Longitude)),
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction
};
centerAnimation.Completed += CenterAnimationCompleted;
BeginAnimation(CenterProperty, centerAnimation, HandoffBehavior.Compose);
}
}
private void CenterAnimationCompleted(object sender, object e)
{
if (centerAnimation != null)
{
centerAnimation.Completed -= CenterAnimationCompleted;
centerAnimation = null;
SetValueInternal(CenterProperty, TargetCenter);
UpdateTransform();
BeginAnimation(CenterProperty, null);
}
}
private void MinZoomLevelPropertyChanged(double minZoomLevel)
{
if (ZoomLevel < minZoomLevel)
centerAnimation = new LocationAnimation
{
ZoomLevel = minZoomLevel;
}
}
To = new Location(targetCenter.Latitude, NearestLongitude(targetCenter.Longitude)),
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction
};
private void MaxZoomLevelPropertyChanged(double maxZoomLevel)
centerAnimation.Completed += CenterAnimationCompleted;
BeginAnimation(CenterProperty, centerAnimation, HandoffBehavior.Compose);
}
}
private void CenterAnimationCompleted(object sender, object e)
{
if (centerAnimation != null)
{
if (ZoomLevel > maxZoomLevel)
{
ZoomLevel = maxZoomLevel;
}
}
centerAnimation.Completed -= CenterAnimationCompleted;
centerAnimation = null;
private void ZoomLevelPropertyChanged(double zoomLevel)
SetValueInternal(CenterProperty, TargetCenter);
UpdateTransform();
BeginAnimation(CenterProperty, null);
}
}
private void MinZoomLevelPropertyChanged(double minZoomLevel)
{
if (ZoomLevel < minZoomLevel)
{
if (!internalPropertyChange)
{
UpdateTransform();
if (zoomLevelAnimation == null)
{
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
}
}
ZoomLevel = minZoomLevel;
}
}
private void TargetZoomLevelPropertyChanged(double targetZoomLevel)
private void MaxZoomLevelPropertyChanged(double maxZoomLevel)
{
if (ZoomLevel > maxZoomLevel)
{
if (!internalPropertyChange && targetZoomLevel != ZoomLevel)
ZoomLevel = maxZoomLevel;
}
}
private void ZoomLevelPropertyChanged(double zoomLevel)
{
if (!internalPropertyChange)
{
UpdateTransform();
if (zoomLevelAnimation == null)
{
if (zoomLevelAnimation != null)
{
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
}
zoomLevelAnimation = new DoubleAnimation
{
To = targetZoomLevel,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction
};
zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted;
BeginAnimation(ZoomLevelProperty, zoomLevelAnimation, HandoffBehavior.Compose);
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
}
}
}
private void ZoomLevelAnimationCompleted(object sender, object e)
private void TargetZoomLevelPropertyChanged(double targetZoomLevel)
{
if (!internalPropertyChange && targetZoomLevel != ZoomLevel)
{
if (zoomLevelAnimation != null)
{
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
zoomLevelAnimation = null;
SetValueInternal(ZoomLevelProperty, TargetZoomLevel);
UpdateTransform(true);
BeginAnimation(ZoomLevelProperty, null);
}
}
private void HeadingPropertyChanged(double heading)
{
if (!internalPropertyChange)
zoomLevelAnimation = new DoubleAnimation
{
UpdateTransform();
To = targetZoomLevel,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction
};
if (headingAnimation == null)
{
SetValueInternal(TargetHeadingProperty, heading);
}
}
zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted;
BeginAnimation(ZoomLevelProperty, zoomLevelAnimation, HandoffBehavior.Compose);
}
}
private void TargetHeadingPropertyChanged(double targetHeading)
private void ZoomLevelAnimationCompleted(object sender, object e)
{
if (zoomLevelAnimation != null)
{
if (!internalPropertyChange && targetHeading != Heading)
{
var delta = targetHeading - Heading;
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
zoomLevelAnimation = null;
if (delta > 180d)
{
delta -= 360d;
}
else if (delta < -180d)
{
delta += 360d;
}
if (headingAnimation != null)
{
headingAnimation.Completed -= HeadingAnimationCompleted;
}
headingAnimation = new DoubleAnimation
{
By = delta,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction
};
headingAnimation.Completed += HeadingAnimationCompleted;
BeginAnimation(HeadingProperty, headingAnimation, HandoffBehavior.SnapshotAndReplace); // don't compose
}
SetValueInternal(ZoomLevelProperty, TargetZoomLevel);
UpdateTransform(true);
BeginAnimation(ZoomLevelProperty, null);
}
}
private void HeadingAnimationCompleted(object sender, object e)
private void HeadingPropertyChanged(double heading)
{
if (!internalPropertyChange)
{
if (headingAnimation != null)
{
headingAnimation.Completed -= HeadingAnimationCompleted;
headingAnimation = null;
UpdateTransform();
SetValueInternal(HeadingProperty, TargetHeading);
UpdateTransform();
BeginAnimation(HeadingProperty, null);
if (headingAnimation == null)
{
SetValueInternal(TargetHeadingProperty, heading);
}
}
}
private void TargetHeadingPropertyChanged(double targetHeading)
{
if (!internalPropertyChange && targetHeading != Heading)
{
var delta = targetHeading - Heading;
if (delta > 180d)
{
delta -= 360d;
}
else if (delta < -180d)
{
delta += 360d;
}
if (headingAnimation != null)
{
headingAnimation.Completed -= HeadingAnimationCompleted;
}
headingAnimation = new DoubleAnimation
{
By = delta,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction
};
headingAnimation.Completed += HeadingAnimationCompleted;
BeginAnimation(HeadingProperty, headingAnimation, HandoffBehavior.SnapshotAndReplace); // don't compose
}
}
private void HeadingAnimationCompleted(object sender, object e)
{
if (headingAnimation != null)
{
headingAnimation.Completed -= HeadingAnimationCompleted;
headingAnimation = null;
SetValueInternal(HeadingProperty, TargetHeading);
UpdateTransform();
BeginAnimation(HeadingProperty, null);
}
}
}

View file

@ -1,29 +1,28 @@
using System.Windows;
namespace MapControl
namespace MapControl;
public partial class MapContentControl
{
public partial class MapContentControl
static MapContentControl()
{
static MapContentControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapContentControl), new FrameworkPropertyMetadata(typeof(MapContentControl)));
}
}
public partial class Pushpin
{
static Pushpin()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Pushpin), new FrameworkPropertyMetadata(typeof(Pushpin)));
}
public static readonly DependencyProperty CornerRadiusProperty =
DependencyPropertyHelper.Register<Pushpin, CornerRadius>(nameof(CornerRadius));
public CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapContentControl), new FrameworkPropertyMetadata(typeof(MapContentControl)));
}
}
public partial class Pushpin
{
static Pushpin()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Pushpin), new FrameworkPropertyMetadata(typeof(Pushpin)));
}
public static readonly DependencyProperty CornerRadiusProperty =
DependencyPropertyHelper.Register<Pushpin, CornerRadius>(nameof(CornerRadius));
public CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
}

View file

@ -4,109 +4,108 @@ using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
namespace MapControl
namespace MapControl;
public partial class MapGrid : FrameworkElement, IMapElement
{
public partial class MapGrid : FrameworkElement, IMapElement
public static readonly DependencyProperty ForegroundProperty =
DependencyPropertyHelper.AddOwner<MapGrid, Brush>(TextElement.ForegroundProperty);
public static readonly DependencyProperty FontFamilyProperty =
DependencyPropertyHelper.AddOwner<MapGrid, FontFamily>(TextElement.FontFamilyProperty);
public static readonly DependencyProperty FontSizeProperty =
DependencyPropertyHelper.AddOwner<MapGrid, double>(TextElement.FontSizeProperty, 12d);
/// <summary>
/// Implements IMapElement.ParentMap.
/// </summary>
public MapBase ParentMap
{
public static readonly DependencyProperty ForegroundProperty =
DependencyPropertyHelper.AddOwner<MapGrid, Brush>(TextElement.ForegroundProperty);
public static readonly DependencyProperty FontFamilyProperty =
DependencyPropertyHelper.AddOwner<MapGrid, FontFamily>(TextElement.FontFamilyProperty);
public static readonly DependencyProperty FontSizeProperty =
DependencyPropertyHelper.AddOwner<MapGrid, double>(TextElement.FontSizeProperty, 12d);
/// <summary>
/// Implements IMapElement.ParentMap.
/// </summary>
public MapBase ParentMap
get;
set
{
get;
set
if (field != null)
{
if (field != null)
{
field.ViewportChanged -= OnViewportChanged;
}
field.ViewportChanged -= OnViewportChanged;
}
field = value;
field = value;
if (field != null)
{
field.ViewportChanged += OnViewportChanged;
}
if (field != null)
{
field.ViewportChanged += OnViewportChanged;
}
}
}
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
{
OnViewportChanged(e);
}
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
{
OnViewportChanged(e);
}
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
{
InvalidateVisual();
}
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
{
InvalidateVisual();
}
protected override void OnRender(DrawingContext drawingContext)
protected override void OnRender(DrawingContext drawingContext)
{
if (ParentMap != null)
{
if (ParentMap != null)
var pathGeometry = new PathGeometry();
var labels = new List<Label>();
var pen = new Pen
{
var pathGeometry = new PathGeometry();
var labels = new List<Label>();
var pen = new Pen
Brush = Foreground,
Thickness = StrokeThickness,
};
DrawGrid(pathGeometry.Figures, labels);
drawingContext.DrawGeometry(null, pen, pathGeometry);
if (labels.Count > 0)
{
var typeface = new Typeface(FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
var pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
foreach (var label in labels)
{
Brush = Foreground,
Thickness = StrokeThickness,
};
var text = new FormattedText(label.Text,
CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, FontSize, Foreground, pixelsPerDip);
var x = label.X +
label.HorizontalAlignment switch
{
HorizontalAlignment.Left => 2d,
HorizontalAlignment.Right => -text.Width - 2d,
_ => -text.Width / 2d
};
var y = label.Y +
label.VerticalAlignment switch
{
VerticalAlignment.Top => 0,
VerticalAlignment.Bottom => -text.Height,
_ => -text.Height / 2d
};
DrawGrid(pathGeometry.Figures, labels);
drawingContext.DrawGeometry(null, pen, pathGeometry);
if (labels.Count > 0)
{
var typeface = new Typeface(FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
var pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
foreach (var label in labels)
if (label.Rotation != 0d)
{
var text = new FormattedText(label.Text,
CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, FontSize, Foreground, pixelsPerDip);
var x = label.X +
label.HorizontalAlignment switch
{
HorizontalAlignment.Left => 2d,
HorizontalAlignment.Right => -text.Width - 2d,
_ => -text.Width / 2d
};
var y = label.Y +
label.VerticalAlignment switch
{
VerticalAlignment.Top => 0,
VerticalAlignment.Bottom => -text.Height,
_ => -text.Height / 2d
};
if (label.Rotation != 0d)
{
drawingContext.PushTransform(new RotateTransform(label.Rotation, label.X, label.Y));
drawingContext.DrawText(text, new Point(x, y));
drawingContext.Pop();
}
else
{
drawingContext.DrawText(text, new Point(x, y));
}
drawingContext.PushTransform(new RotateTransform(label.Rotation, label.X, label.Y));
drawingContext.DrawText(text, new Point(x, y));
drawingContext.Pop();
}
else
{
drawingContext.DrawText(text, new Point(x, y));
}
}
}
}
}
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
{
return new PolyLineSegment(points, true);
}
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
{
return new PolyLineSegment(points, true);
}
}

View file

@ -2,35 +2,34 @@
using System.Windows;
using System.Windows.Media.Animation;
namespace MapControl
namespace MapControl;
public partial class MapImageLayer
{
public partial class MapImageLayer
private void FadeOver()
{
private void FadeOver()
var fadeInAnimation = new DoubleAnimation
{
var fadeInAnimation = new DoubleAnimation
{
To = 1d,
Duration = MapBase.ImageFadeDuration
};
To = 1d,
Duration = MapBase.ImageFadeDuration
};
var fadeOutAnimation = new DoubleAnimation
{
To = 0d,
BeginTime = MapBase.ImageFadeDuration,
Duration = TimeSpan.Zero
};
var fadeOutAnimation = new DoubleAnimation
{
To = 0d,
BeginTime = MapBase.ImageFadeDuration,
Duration = TimeSpan.Zero
};
Storyboard.SetTarget(fadeInAnimation, Children[1]);
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(OpacityProperty));
Storyboard.SetTarget(fadeInAnimation, Children[1]);
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(OpacityProperty));
Storyboard.SetTarget(fadeOutAnimation, Children[0]);
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(OpacityProperty));
Storyboard.SetTarget(fadeOutAnimation, Children[0]);
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(OpacityProperty));
var storyboard = new Storyboard();
storyboard.Children.Add(fadeInAnimation);
storyboard.Children.Add(fadeOutAnimation);
storyboard.Begin();
}
var storyboard = new Storyboard();
storyboard.Children.Add(fadeInAnimation);
storyboard.Children.Add(fadeOutAnimation);
storyboard.Begin();
}
}

View file

@ -2,38 +2,37 @@
using System.Windows.Controls;
using System.Windows.Input;
namespace MapControl
namespace MapControl;
public partial class MapItem
{
public partial class MapItem
static MapItem()
{
static MapItem()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItem), new FrameworkPropertyMetadata(typeof(MapItem)));
}
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItem), new FrameworkPropertyMetadata(typeof(MapItem)));
}
protected override void OnTouchDown(TouchEventArgs e)
protected override void OnTouchDown(TouchEventArgs e)
{
e.Handled = true;
}
protected override void OnTouchUp(TouchEventArgs e)
{
e.Handled = true;
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) &&
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl &&
mapItemsControl.SelectionMode == SelectionMode.Extended)
{
mapItemsControl.SelectItemsInRange(this);
e.Handled = true;
}
protected override void OnTouchUp(TouchEventArgs e)
else
{
e.Handled = true;
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) &&
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl &&
mapItemsControl.SelectionMode == SelectionMode.Extended)
{
mapItemsControl.SelectItemsInRange(this);
e.Handled = true;
}
else
{
base.OnMouseLeftButtonDown(e);
}
base.OnMouseLeftButtonDown(e);
}
}
}

View file

@ -2,50 +2,49 @@
using System.Windows.Controls;
using System.Windows.Media;
namespace MapControl
namespace MapControl;
public partial class MapItemsControl
{
public partial class MapItemsControl
static MapItemsControl()
{
static MapItemsControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItemsControl), new FrameworkPropertyMetadata(typeof(MapItemsControl)));
}
DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItemsControl), new FrameworkPropertyMetadata(typeof(MapItemsControl)));
}
public void SelectItemsInGeometry(Geometry geometry)
{
SelectItemsByPosition(geometry.FillContains);
}
public void SelectItemsInGeometry(Geometry geometry)
{
SelectItemsByPosition(geometry.FillContains);
}
public MapItem ContainerFromItem(object item)
{
return (MapItem)ItemContainerGenerator.ContainerFromItem(item);
}
public MapItem ContainerFromItem(object item)
{
return (MapItem)ItemContainerGenerator.ContainerFromItem(item);
}
public object ItemFromContainer(MapItem container)
{
return ItemContainerGenerator.ItemFromContainer(container);
}
public object ItemFromContainer(MapItem container)
{
return ItemContainerGenerator.ItemFromContainer(container);
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is MapItem;
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is MapItem;
}
protected override DependencyObject GetContainerForItemOverride()
{
return new MapItem();
}
protected override DependencyObject GetContainerForItemOverride()
{
return new MapItem();
}
protected override void PrepareContainerForItemOverride(DependencyObject container, object item)
{
base.PrepareContainerForItemOverride(container, item);
PrepareContainer((MapItem)container, item);
}
protected override void PrepareContainerForItemOverride(DependencyObject container, object item)
{
base.PrepareContainerForItemOverride(container, item);
PrepareContainer((MapItem)container, item);
}
protected override void ClearContainerForItemOverride(DependencyObject container, object item)
{
base.ClearContainerForItemOverride(container, item);
ClearContainer((MapItem)container);
}
protected override void ClearContainerForItemOverride(DependencyObject container, object item)
{
base.ClearContainerForItemOverride(container, item);
ClearContainer((MapItem)container);
}
}

View file

@ -1,27 +1,26 @@
using System.Windows;
namespace MapControl
namespace MapControl;
public partial class MapPanel
{
public partial class MapPanel
public static readonly DependencyProperty AutoCollapseProperty =
DependencyPropertyHelper.RegisterAttached< bool>("AutoCollapse", typeof(MapPanel));
public static readonly DependencyProperty LocationProperty =
DependencyPropertyHelper.RegisterAttached<Location>("Location", typeof(MapPanel), null,
FrameworkPropertyMetadataOptions.AffectsParentArrange);
public static readonly DependencyProperty BoundingBoxProperty =
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel), null,
FrameworkPropertyMetadataOptions.AffectsParentArrange);
public static readonly DependencyProperty MapRectProperty =
DependencyPropertyHelper.RegisterAttached<Rect?>("MapRect", typeof(MapPanel), null,
FrameworkPropertyMetadataOptions.AffectsParentArrange);
public static MapBase GetParentMap(FrameworkElement element)
{
public static readonly DependencyProperty AutoCollapseProperty =
DependencyPropertyHelper.RegisterAttached< bool>("AutoCollapse", typeof(MapPanel));
public static readonly DependencyProperty LocationProperty =
DependencyPropertyHelper.RegisterAttached<Location>("Location", typeof(MapPanel), null,
FrameworkPropertyMetadataOptions.AffectsParentArrange);
public static readonly DependencyProperty BoundingBoxProperty =
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel), null,
FrameworkPropertyMetadataOptions.AffectsParentArrange);
public static readonly DependencyProperty MapRectProperty =
DependencyPropertyHelper.RegisterAttached<Rect?>("MapRect", typeof(MapPanel), null,
FrameworkPropertyMetadataOptions.AffectsParentArrange);
public static MapBase GetParentMap(FrameworkElement element)
{
return (MapBase)element.GetValue(ParentMapProperty);
}
return (MapBase)element.GetValue(ParentMapProperty);
}
}

View file

@ -2,36 +2,35 @@
using System.Windows.Media;
using System.Windows.Shapes;
namespace MapControl
namespace MapControl;
public partial class MapPath : Shape
{
public partial class MapPath : Shape
public static readonly DependencyProperty DataProperty =
DependencyPropertyHelper.AddOwner<MapPath, Geometry>(Path.DataProperty,
(path, oldValue, newValue) => path.DataPropertyChanged(oldValue, newValue));
public Geometry Data
{
public static readonly DependencyProperty DataProperty =
DependencyPropertyHelper.AddOwner<MapPath, Geometry>(Path.DataProperty,
(path, oldValue, newValue) => path.DataPropertyChanged(oldValue, newValue));
get => (Geometry)GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
public Geometry Data
protected override Geometry DefiningGeometry => Data;
private void DataPropertyChanged(Geometry oldValue, Geometry newValue)
{
// Check if Data is actually a new Geometry.
//
if (newValue != null && !ReferenceEquals(newValue, oldValue))
{
get => (Geometry)GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
protected override Geometry DefiningGeometry => Data;
private void DataPropertyChanged(Geometry oldValue, Geometry newValue)
{
// Check if Data is actually a new Geometry.
//
if (newValue != null && !ReferenceEquals(newValue, oldValue))
if (newValue.IsFrozen)
{
if (newValue.IsFrozen)
{
Data = newValue.Clone(); // DataPropertyChanged called again
}
else
{
UpdateData();
}
Data = newValue.Clone(); // DataPropertyChanged called again
}
else
{
UpdateData();
}
}
}

View file

@ -3,64 +3,63 @@ using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
namespace MapControl
namespace MapControl;
public partial class MapPolypoint : MapPath
{
public partial class MapPolypoint : MapPath
protected void UpdateData(IEnumerable<Location> locations, bool closed)
{
protected void UpdateData(IEnumerable<Location> locations, bool closed)
using var context = ((StreamGeometry)Data).Open();
if (ParentMap != null && locations != null)
{
using var context = ((StreamGeometry)Data).Open();
var longitudeOffset = GetLongitudeOffset(locations);
if (ParentMap != null && locations != null)
AddPolylinePoints(context, locations, longitudeOffset, closed);
}
}
protected void UpdateData(IEnumerable<IEnumerable<Location>> polygons)
{
using var context = ((StreamGeometry)Data).Open();
if (ParentMap != null && polygons != null)
{
var longitudeOffset = GetLongitudeOffset(polygons.FirstOrDefault());
foreach (var locations in polygons)
{
var longitudeOffset = GetLongitudeOffset(locations);
AddPolylinePoints(context, locations, longitudeOffset, closed);
AddPolylinePoints(context, locations, longitudeOffset, true);
}
}
}
protected void UpdateData(IEnumerable<IEnumerable<Location>> polygons)
private void AddPolylinePoints(StreamGeometryContext context, IEnumerable<Location> locations, double longitudeOffset, bool closed)
{
var points = locations.Select(location => LocationToView(location, longitudeOffset));
if (points.Any())
{
using var context = ((StreamGeometry)Data).Open();
var start = points.First();
var polyline = points.Skip(1).ToList();
var minX = start.X;
var maxX = start.X;
var minY = start.Y;
var maxY = start.Y;
if (ParentMap != null && polygons != null)
foreach (var point in polyline)
{
var longitudeOffset = GetLongitudeOffset(polygons.FirstOrDefault());
foreach (var locations in polygons)
{
AddPolylinePoints(context, locations, longitudeOffset, true);
}
minX = Math.Min(minX, point.X);
maxX = Math.Max(maxX, point.X);
minY = Math.Min(minY, point.Y);
maxY = Math.Max(maxY, point.Y);
}
}
private void AddPolylinePoints(StreamGeometryContext context, IEnumerable<Location> locations, double longitudeOffset, bool closed)
{
var points = locations.Select(location => LocationToView(location, longitudeOffset));
if (points.Any())
if (maxX >= 0d && minX <= ParentMap.ActualWidth &&
maxY >= 0d && minY <= ParentMap.ActualHeight)
{
var start = points.First();
var polyline = points.Skip(1).ToList();
var minX = start.X;
var maxX = start.X;
var minY = start.Y;
var maxY = start.Y;
foreach (var point in polyline)
{
minX = Math.Min(minX, point.X);
maxX = Math.Max(maxX, point.X);
minY = Math.Min(minY, point.Y);
maxY = Math.Max(maxY, point.Y);
}
if (maxX >= 0d && minX <= ParentMap.ActualWidth &&
maxY >= 0d && minY <= ParentMap.ActualHeight)
{
context.BeginFigure(start, true, closed);
context.PolyLineTo(polyline, true, true);
}
context.BeginFigure(start, true, closed);
context.PolyLineTo(polyline, true, true);
}
}
}

View file

@ -3,104 +3,103 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace MapControl
namespace MapControl;
public partial class PushpinBorder : Decorator
{
public partial class PushpinBorder : Decorator
public static readonly DependencyProperty ArrowSizeProperty =
DependencyPropertyHelper.Register<PushpinBorder, Size>(nameof(ArrowSize), new Size(10d, 20d),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender);
public static readonly DependencyProperty BorderWidthProperty =
DependencyPropertyHelper.Register<PushpinBorder, double>(nameof(BorderWidth), 0d,
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender);
public static readonly DependencyProperty BackgroundProperty =
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(Background), null,
FrameworkPropertyMetadataOptions.AffectsRender);
public static readonly DependencyProperty BorderBrushProperty =
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(BorderBrush), null,
FrameworkPropertyMetadataOptions.AffectsRender);
public static readonly DependencyProperty CornerRadiusProperty =
DependencyPropertyHelper.Register<PushpinBorder, CornerRadius>(nameof(CornerRadius), new CornerRadius(),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender);
public static readonly DependencyProperty PaddingProperty =
DependencyPropertyHelper.Register<PushpinBorder, Thickness>(nameof(Padding), new Thickness(2),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender);
public Brush Background
{
public static readonly DependencyProperty ArrowSizeProperty =
DependencyPropertyHelper.Register<PushpinBorder, Size>(nameof(ArrowSize), new Size(10d, 20d),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender);
get => (Brush)GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public static readonly DependencyProperty BorderWidthProperty =
DependencyPropertyHelper.Register<PushpinBorder, double>(nameof(BorderWidth), 0d,
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender);
public Brush BorderBrush
{
get => (Brush)GetValue(BorderBrushProperty);
set => SetValue(BorderBrushProperty, value);
}
public static readonly DependencyProperty BackgroundProperty =
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(Background), null,
FrameworkPropertyMetadataOptions.AffectsRender);
public CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public static readonly DependencyProperty BorderBrushProperty =
DependencyPropertyHelper.Register<PushpinBorder, Brush>(nameof(BorderBrush), null,
FrameworkPropertyMetadataOptions.AffectsRender);
public Thickness Padding
{
get => (Thickness)GetValue(PaddingProperty);
set => SetValue(PaddingProperty, value);
}
public static readonly DependencyProperty CornerRadiusProperty =
DependencyPropertyHelper.Register<PushpinBorder, CornerRadius>(nameof(CornerRadius), new CornerRadius(),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender);
protected override Size MeasureOverride(Size constraint)
{
var width = 2d * BorderWidth + Padding.Left + Padding.Right;
var height = 2d * BorderWidth + Padding.Top + Padding.Bottom;
public static readonly DependencyProperty PaddingProperty =
DependencyPropertyHelper.Register<PushpinBorder, Thickness>(nameof(Padding), new Thickness(2),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender);
public Brush Background
if (Child != null)
{
get => (Brush)GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
Child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
width += Child.DesiredSize.Width;
height += Child.DesiredSize.Height;
}
public Brush BorderBrush
var minWidth = BorderWidth + Math.Max(
CornerRadius.TopLeft + CornerRadius.TopRight,
CornerRadius.BottomLeft + CornerRadius.BottomRight + ArrowSize.Width);
var minHeight = BorderWidth + Math.Max(
CornerRadius.TopLeft + CornerRadius.BottomLeft,
CornerRadius.TopRight + CornerRadius.BottomRight);
return new Size(
Math.Max(width, minWidth),
Math.Max(height, minHeight) + ArrowSize.Height);
}
protected override Size ArrangeOverride(Size size)
{
Child?.Arrange(new Rect(
BorderWidth + Padding.Left,
BorderWidth + Padding.Top,
Child.DesiredSize.Width,
Child.DesiredSize.Height));
return DesiredSize;
}
protected override void OnRender(DrawingContext drawingContext)
{
var pen = new Pen
{
get => (Brush)GetValue(BorderBrushProperty);
set => SetValue(BorderBrushProperty, value);
}
Brush = BorderBrush,
Thickness = BorderWidth,
LineJoin = PenLineJoin.Round
};
public CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public Thickness Padding
{
get => (Thickness)GetValue(PaddingProperty);
set => SetValue(PaddingProperty, value);
}
protected override Size MeasureOverride(Size constraint)
{
var width = 2d * BorderWidth + Padding.Left + Padding.Right;
var height = 2d * BorderWidth + Padding.Top + Padding.Bottom;
if (Child != null)
{
Child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
width += Child.DesiredSize.Width;
height += Child.DesiredSize.Height;
}
var minWidth = BorderWidth + Math.Max(
CornerRadius.TopLeft + CornerRadius.TopRight,
CornerRadius.BottomLeft + CornerRadius.BottomRight + ArrowSize.Width);
var minHeight = BorderWidth + Math.Max(
CornerRadius.TopLeft + CornerRadius.BottomLeft,
CornerRadius.TopRight + CornerRadius.BottomRight);
return new Size(
Math.Max(width, minWidth),
Math.Max(height, minHeight) + ArrowSize.Height);
}
protected override Size ArrangeOverride(Size size)
{
Child?.Arrange(new Rect(
BorderWidth + Padding.Left,
BorderWidth + Padding.Top,
Child.DesiredSize.Width,
Child.DesiredSize.Height));
return DesiredSize;
}
protected override void OnRender(DrawingContext drawingContext)
{
var pen = new Pen
{
Brush = BorderBrush,
Thickness = BorderWidth,
LineJoin = PenLineJoin.Round
};
drawingContext.DrawGeometry(Background, pen, BuildGeometry());
}
drawingContext.DrawGeometry(Background, pen, BuildGeometry());
}
}

View file

@ -5,55 +5,54 @@ using Windows.UI.Xaml;
using Microsoft.UI.Xaml;
#endif
namespace MapControl
namespace MapControl;
public static class DependencyPropertyHelper
{
public static class DependencyPropertyHelper
public static DependencyProperty RegisterAttached<TValue>(
string name,
Type ownerType,
TValue defaultValue = default,
Action<FrameworkElement, TValue, TValue> changed = null)
{
public static DependencyProperty RegisterAttached<TValue>(
string name,
Type ownerType,
TValue defaultValue = default,
Action<FrameworkElement, TValue, TValue> changed = null)
{
var metadata = changed == null
? new PropertyMetadata(defaultValue)
: new PropertyMetadata(defaultValue, (o, e) =>
{
if (o is FrameworkElement element)
{
changed(element, (TValue)e.OldValue, (TValue)e.NewValue);
}
});
return DependencyProperty.RegisterAttached(name, typeof(TValue), ownerType, metadata);
}
public static DependencyProperty Register<TOwner, TValue>(
string name,
TValue defaultValue = default,
Action<TOwner, TValue, TValue> changed = null)
where TOwner : DependencyObject
{
var metadata = changed != null
? new PropertyMetadata(defaultValue, (o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue))
: new PropertyMetadata(defaultValue);
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
}
public static DependencyProperty AddOwner<TOwner, TValue>(
string name,
DependencyProperty source,
Action<TOwner, TValue, TValue> changed = null)
where TOwner : DependencyObject
{
var metadata = new PropertyMetadata(default, (o, e) =>
var metadata = changed == null
? new PropertyMetadata(defaultValue)
: new PropertyMetadata(defaultValue, (o, e) =>
{
o.SetValue(source, e.NewValue);
changed?.Invoke((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue);
if (o is FrameworkElement element)
{
changed(element, (TValue)e.OldValue, (TValue)e.NewValue);
}
});
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
}
return DependencyProperty.RegisterAttached(name, typeof(TValue), ownerType, metadata);
}
public static DependencyProperty Register<TOwner, TValue>(
string name,
TValue defaultValue = default,
Action<TOwner, TValue, TValue> changed = null)
where TOwner : DependencyObject
{
var metadata = changed != null
? new PropertyMetadata(defaultValue, (o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue))
: new PropertyMetadata(defaultValue);
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
}
public static DependencyProperty AddOwner<TOwner, TValue>(
string name,
DependencyProperty source,
Action<TOwner, TValue, TValue> changed = null)
where TOwner : DependencyObject
{
var metadata = new PropertyMetadata(default, (o, e) =>
{
o.SetValue(source, e.NewValue);
changed?.Invoke((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue);
});
return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata);
}
}

View file

@ -8,66 +8,65 @@ using Windows.UI.Xaml.Media.Imaging;
using Microsoft.UI.Xaml.Media.Imaging;
#endif
namespace MapControl
namespace MapControl;
public static partial class GeoImage
{
public static partial class GeoImage
private static async Task<GeoBitmap> LoadGeoTiff(string sourcePath)
{
private static async Task<GeoBitmap> LoadGeoTiff(string sourcePath)
BitmapSource bitmap;
Matrix transform;
MapProjection projection = null;
var file = await StorageFile.GetFileFromPathAsync(FilePath.GetFullPath(sourcePath));
using var stream = await file.OpenReadAsync();
var decoder = await BitmapDecoder.CreateAsync(stream);
bitmap = await ImageLoader.LoadWriteableBitmapAsync(decoder);
var geoKeyDirectoryQuery = QueryString(GeoKeyDirectoryTag);
var pixelScaleQuery = QueryString(ModelPixelScaleTag);
var tiePointQuery = QueryString(ModelTiePointTag);
var transformationQuery = QueryString(ModelTransformationTag);
var metadata = await decoder.BitmapProperties.GetPropertiesAsync(
new string[]
{
pixelScaleQuery,
tiePointQuery,
transformationQuery,
geoKeyDirectoryQuery
});
if (metadata.TryGetValue(pixelScaleQuery, out BitmapTypedValue pixelScaleValue) &&
pixelScaleValue.Value is double[] pixelScale &&
pixelScale.Length == 3 &&
metadata.TryGetValue(tiePointQuery, out BitmapTypedValue tiePointValue) &&
tiePointValue.Value is double[] tiePoint &&
tiePoint.Length >= 6)
{
BitmapSource bitmap;
Matrix transform;
MapProjection projection = null;
var file = await StorageFile.GetFileFromPathAsync(FilePath.GetFullPath(sourcePath));
using var stream = await file.OpenReadAsync();
var decoder = await BitmapDecoder.CreateAsync(stream);
bitmap = await ImageLoader.LoadWriteableBitmapAsync(decoder);
var geoKeyDirectoryQuery = QueryString(GeoKeyDirectoryTag);
var pixelScaleQuery = QueryString(ModelPixelScaleTag);
var tiePointQuery = QueryString(ModelTiePointTag);
var transformationQuery = QueryString(ModelTransformationTag);
var metadata = await decoder.BitmapProperties.GetPropertiesAsync(
new string[]
{
pixelScaleQuery,
tiePointQuery,
transformationQuery,
geoKeyDirectoryQuery
});
if (metadata.TryGetValue(pixelScaleQuery, out BitmapTypedValue pixelScaleValue) &&
pixelScaleValue.Value is double[] pixelScale &&
pixelScale.Length == 3 &&
metadata.TryGetValue(tiePointQuery, out BitmapTypedValue tiePointValue) &&
tiePointValue.Value is double[] tiePoint &&
tiePoint.Length >= 6)
{
transform = new Matrix(pixelScale[0], 0d, 0d, -pixelScale[1], tiePoint[3], tiePoint[4]);
}
else if (metadata.TryGetValue(transformationQuery, out BitmapTypedValue transformValue) &&
transformValue.Value is double[] transformValues &&
transformValues.Length == 16)
{
transform = new Matrix(transformValues[0], transformValues[1],
transformValues[4], transformValues[5],
transformValues[3], transformValues[7]);
}
else
{
throw new ArgumentException("No coordinate transformation found.");
}
if (metadata.TryGetValue(geoKeyDirectoryQuery, out BitmapTypedValue geoKeyDirValue) &&
geoKeyDirValue.Value is short[] geoKeyDirectory)
{
projection = GetProjection(geoKeyDirectory);
}
return new GeoBitmap(bitmap, transform, projection);
transform = new Matrix(pixelScale[0], 0d, 0d, -pixelScale[1], tiePoint[3], tiePoint[4]);
}
else if (metadata.TryGetValue(transformationQuery, out BitmapTypedValue transformValue) &&
transformValue.Value is double[] transformValues &&
transformValues.Length == 16)
{
transform = new Matrix(transformValues[0], transformValues[1],
transformValues[4], transformValues[5],
transformValues[3], transformValues[7]);
}
else
{
throw new ArgumentException("No coordinate transformation found.");
}
if (metadata.TryGetValue(geoKeyDirectoryQuery, out BitmapTypedValue geoKeyDirValue) &&
geoKeyDirValue.Value is short[] geoKeyDirectory)
{
projection = GetProjection(geoKeyDirectory);
}
return new GeoBitmap(bitmap, transform, projection);
}
}

View file

@ -14,126 +14,125 @@ using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
#endif
namespace MapControl
namespace MapControl;
public static partial class ImageLoader
{
public static partial class ImageLoader
public static ImageSource LoadResourceImage(Uri uri)
{
public static ImageSource LoadResourceImage(Uri uri)
return new BitmapImage(uri);
}
public static async Task<ImageSource> LoadImageAsync(IRandomAccessStream randomAccessStream)
{
var image = new BitmapImage();
await image.SetSourceAsync(randomAccessStream);
return image;
}
public static async Task<ImageSource> LoadImageAsync(Stream stream)
{
using var randomAccessStream = stream.AsRandomAccessStream();
return await LoadImageAsync(randomAccessStream);
}
public static async Task<ImageSource> LoadImageAsync(string path)
{
ImageSource image = null;
path = FilePath.GetFullPath(path);
if (File.Exists(path))
{
return new BitmapImage(uri);
var file = await StorageFile.GetFileFromPathAsync(path);
using var randomAccessStream = await file.OpenReadAsync();
image = await LoadImageAsync(randomAccessStream);
}
public static async Task<ImageSource> LoadImageAsync(IRandomAccessStream randomAccessStream)
return image;
}
internal static async Task<WriteableBitmap> LoadWriteableBitmapAsync(BitmapDecoder decoder)
{
var image = new WriteableBitmap((int)decoder.PixelWidth, (int)decoder.PixelHeight);
var pixelData = await decoder.GetPixelDataAsync(
BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied, new BitmapTransform(),
ExifOrientationMode.IgnoreExifOrientation, ColorManagementMode.DoNotColorManage);
pixelData.DetachPixelData().CopyTo(image.PixelBuffer);
return image;
}
internal static async Task<WriteableBitmap> LoadWriteableBitmapAsync(Uri uri, IProgress<double> progress)
{
WriteableBitmap bitmap = null;
progress.Report(0d);
try
{
var image = new BitmapImage();
var buffer = await GetHttpContent(uri, progress);
await image.SetSourceAsync(randomAccessStream);
return image;
}
public static async Task<ImageSource> LoadImageAsync(Stream stream)
{
using var randomAccessStream = stream.AsRandomAccessStream();
return await LoadImageAsync(randomAccessStream);
}
public static async Task<ImageSource> LoadImageAsync(string path)
{
ImageSource image = null;
path = FilePath.GetFullPath(path);
if (File.Exists(path))
if (buffer != null)
{
var file = await StorageFile.GetFileFromPathAsync(path);
using var memoryStream = new MemoryStream(buffer);
using var randomAccessStream = memoryStream.AsRandomAccessStream();
using var randomAccessStream = await file.OpenReadAsync();
var decoder = await BitmapDecoder.CreateAsync(randomAccessStream);
image = await LoadImageAsync(randomAccessStream);
bitmap = await LoadWriteableBitmapAsync(decoder);
}
return image;
}
internal static async Task<WriteableBitmap> LoadWriteableBitmapAsync(BitmapDecoder decoder)
catch (Exception ex)
{
var image = new WriteableBitmap((int)decoder.PixelWidth, (int)decoder.PixelHeight);
var pixelData = await decoder.GetPixelDataAsync(
BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied, new BitmapTransform(),
ExifOrientationMode.IgnoreExifOrientation, ColorManagementMode.DoNotColorManage);
pixelData.DetachPixelData().CopyTo(image.PixelBuffer);
return image;
Logger?.LogError(ex, "Failed loading {uri}", uri);
}
internal static async Task<WriteableBitmap> LoadWriteableBitmapAsync(Uri uri, IProgress<double> progress)
progress.Report(1d);
return bitmap;
}
internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
{
WriteableBitmap mergedBitmap = null;
var p1 = 0d;
var p2 = 0d;
var bitmaps = await Task.WhenAll(
LoadWriteableBitmapAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
LoadWriteableBitmapAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
if (bitmaps.Length == 2 &&
bitmaps[0] != null &&
bitmaps[1] != null &&
bitmaps[0].PixelHeight == bitmaps[1].PixelHeight)
{
WriteableBitmap bitmap = null;
var buffer1 = bitmaps[0].PixelBuffer;
var buffer2 = bitmaps[1].PixelBuffer;
var stride1 = (uint)bitmaps[0].PixelWidth * 4;
var stride2 = (uint)bitmaps[1].PixelWidth * 4;
var stride = stride1 + stride2;
var height = bitmaps[0].PixelHeight;
progress.Report(0d);
mergedBitmap = new WriteableBitmap(bitmaps[0].PixelWidth + bitmaps[1].PixelWidth, height);
try
var buffer = mergedBitmap.PixelBuffer;
for (uint y = 0; y < height; y++)
{
var buffer = await GetHttpContent(uri, progress);
if (buffer != null)
{
using var memoryStream = new MemoryStream(buffer);
using var randomAccessStream = memoryStream.AsRandomAccessStream();
var decoder = await BitmapDecoder.CreateAsync(randomAccessStream);
bitmap = await LoadWriteableBitmapAsync(decoder);
}
buffer1.CopyTo(y * stride1, buffer, y * stride, stride1);
buffer2.CopyTo(y * stride2, buffer, y * stride + stride1, stride2);
}
catch (Exception ex)
{
Logger?.LogError(ex, "Failed loading {uri}", uri);
}
progress.Report(1d);
return bitmap;
}
internal static async Task<ImageSource> LoadMergedImageAsync(Uri uri1, Uri uri2, IProgress<double> progress)
{
WriteableBitmap mergedBitmap = null;
var p1 = 0d;
var p2 = 0d;
var bitmaps = await Task.WhenAll(
LoadWriteableBitmapAsync(uri1, new Progress<double>(p => { p1 = p; progress.Report((p1 + p2) / 2d); })),
LoadWriteableBitmapAsync(uri2, new Progress<double>(p => { p2 = p; progress.Report((p1 + p2) / 2d); })));
if (bitmaps.Length == 2 &&
bitmaps[0] != null &&
bitmaps[1] != null &&
bitmaps[0].PixelHeight == bitmaps[1].PixelHeight)
{
var buffer1 = bitmaps[0].PixelBuffer;
var buffer2 = bitmaps[1].PixelBuffer;
var stride1 = (uint)bitmaps[0].PixelWidth * 4;
var stride2 = (uint)bitmaps[1].PixelWidth * 4;
var stride = stride1 + stride2;
var height = bitmaps[0].PixelHeight;
mergedBitmap = new WriteableBitmap(bitmaps[0].PixelWidth + bitmaps[1].PixelWidth, height);
var buffer = mergedBitmap.PixelBuffer;
for (uint y = 0; y < height; y++)
{
buffer1.CopyTo(y * stride1, buffer, y * stride, stride1);
buffer2.CopyTo(y * stride2, buffer, y * stride + stride1, stride2);
}
}
return mergedBitmap;
}
return mergedBitmap;
}
}

View file

@ -16,92 +16,91 @@ using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Media.Imaging;
#endif
namespace MapControl
namespace MapControl;
public class ImageTile(int zoomLevel, int x, int y, int columnCount)
: Tile(zoomLevel, x, y, columnCount)
{
public class ImageTile(int zoomLevel, int x, int y, int columnCount)
: Tile(zoomLevel, x, y, columnCount)
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
{
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
var tcs = new TaskCompletionSource<object>();
public override async Task LoadImageAsync(Func<Task<ImageSource>> loadImageFunc)
async void LoadAndSetImageSource()
{
var tcs = new TaskCompletionSource<object>();
async void LoadAndSetImageSource()
try
{
try
var image = await loadImageFunc();
Image.Source = image;
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
{
var image = await loadImageFunc();
Image.Source = image;
if (image != null && MapBase.ImageFadeDuration > TimeSpan.Zero)
if (image is BitmapImage bitmap && bitmap.UriSource != null)
{
if (image is BitmapImage bitmap && bitmap.UriSource != null)
{
bitmap.ImageOpened += BitmapImageOpened;
bitmap.ImageFailed += BitmapImageFailed;
}
else
{
BeginFadeInAnimation();
}
bitmap.ImageOpened += BitmapImageOpened;
bitmap.ImageFailed += BitmapImageFailed;
}
else
{
BeginFadeInAnimation();
}
}
tcs.TrySetResult(null);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
tcs.TrySetResult(null);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}
#if UWP
if (!await Image.Dispatcher.TryRunAsync(CoreDispatcherPriority.Low, LoadAndSetImageSource))
if (!await Image.Dispatcher.TryRunAsync(CoreDispatcherPriority.Low, LoadAndSetImageSource))
#else
if (!Image.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, LoadAndSetImageSource))
if (!Image.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, LoadAndSetImageSource))
#endif
{
tcs.TrySetCanceled();
}
await tcs.Task;
}
private void BeginFadeInAnimation()
{
var fadeInAnimation = new DoubleAnimation
{
From = 0d,
Duration = MapBase.ImageFadeDuration,
FillBehavior = FillBehavior.Stop
};
Storyboard.SetTarget(fadeInAnimation, Image);
Storyboard.SetTargetProperty(fadeInAnimation, nameof(UIElement.Opacity));
var storyboard = new Storyboard();
storyboard.Children.Add(fadeInAnimation);
storyboard.Begin();
tcs.TrySetCanceled();
}
private void BitmapImageOpened(object sender, RoutedEventArgs e)
await tcs.Task;
}
private void BeginFadeInAnimation()
{
var fadeInAnimation = new DoubleAnimation
{
var bitmap = (BitmapImage)sender;
From = 0d,
Duration = MapBase.ImageFadeDuration,
FillBehavior = FillBehavior.Stop
};
bitmap.ImageOpened -= BitmapImageOpened;
bitmap.ImageFailed -= BitmapImageFailed;
Storyboard.SetTarget(fadeInAnimation, Image);
Storyboard.SetTargetProperty(fadeInAnimation, nameof(UIElement.Opacity));
BeginFadeInAnimation();
}
var storyboard = new Storyboard();
storyboard.Children.Add(fadeInAnimation);
storyboard.Begin();
}
private void BitmapImageFailed(object sender, ExceptionRoutedEventArgs e)
{
var bitmap = (BitmapImage)sender;
private void BitmapImageOpened(object sender, RoutedEventArgs e)
{
var bitmap = (BitmapImage)sender;
bitmap.ImageOpened -= BitmapImageOpened;
bitmap.ImageFailed -= BitmapImageFailed;
bitmap.ImageOpened -= BitmapImageOpened;
bitmap.ImageFailed -= BitmapImageFailed;
Image.Source = null;
}
BeginFadeInAnimation();
}
private void BitmapImageFailed(object sender, ExceptionRoutedEventArgs e)
{
var bitmap = (BitmapImage)sender;
bitmap.ImageOpened -= BitmapImageOpened;
bitmap.ImageFailed -= BitmapImageFailed;
Image.Source = null;
}
}

Some files were not shown because too many files have changed in this diff Show more