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

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

View file

@ -7,65 +7,64 @@ using Microsoft.UI.Input;
using Microsoft.UI.Xaml.Input;
#endif
namespace MapControl
namespace MapControl;
public partial class Map
{
public partial class Map
public Map()
{
public Map()
ManipulationMode
= ManipulationModes.Scale
| ManipulationModes.TranslateX
| ManipulationModes.TranslateY
| ManipulationModes.TranslateInertia;
PointerWheelChanged += OnPointerWheelChanged;
PointerMoved += OnPointerMoved;
ManipulationDelta += OnManipulationDelta;
ManipulationCompleted += OnManipulationCompleted;
}
private void OnPointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
if (e.Pointer.PointerDeviceType == PointerDeviceType.Mouse)
{
ManipulationMode
= ManipulationModes.Scale
| ManipulationModes.TranslateX
| ManipulationModes.TranslateY
| ManipulationModes.TranslateInertia;
var point = e.GetCurrentPoint(this);
PointerWheelChanged += OnPointerWheelChanged;
PointerMoved += OnPointerMoved;
ManipulationDelta += OnManipulationDelta;
ManipulationCompleted += OnManipulationCompleted;
}
private void OnPointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
if (e.Pointer.PointerDeviceType == PointerDeviceType.Mouse)
{
var point = e.GetCurrentPoint(this);
// Standard mouse wheel delta value is 120.
//
OnMouseWheel(point.Position, point.Properties.MouseWheelDelta / 120d);
}
}
private bool? manipulationEnabled;
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (!manipulationEnabled.HasValue &&
e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
manipulationEnabled = e.KeyModifiers == VirtualKeyModifiers.None;
}
}
private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
if (manipulationEnabled.HasValue && manipulationEnabled.Value)
{
if (e.PointerDeviceType == PointerDeviceType.Mouse)
{
TranslateMap(e.Delta.Translation);
}
else
{
TransformMap(e.Position, e.Delta.Translation, e.Delta.Rotation, e.Delta.Scale);
}
}
}
private void OnManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
manipulationEnabled = null;
// Standard mouse wheel delta value is 120.
//
OnMouseWheel(point.Position, point.Properties.MouseWheelDelta / 120d);
}
}
private bool? manipulationEnabled;
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (!manipulationEnabled.HasValue &&
e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
manipulationEnabled = e.KeyModifiers == VirtualKeyModifiers.None;
}
}
private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
if (manipulationEnabled.HasValue && manipulationEnabled.Value)
{
if (e.PointerDeviceType == PointerDeviceType.Mouse)
{
TranslateMap(e.Delta.Translation);
}
else
{
TransformMap(e.Position, e.Delta.Translation, e.Delta.Rotation, e.Delta.Scale);
}
}
}
private void OnManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
manipulationEnabled = null;
}
}

View file

@ -10,355 +10,354 @@ using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
#endif
namespace MapControl
namespace MapControl;
public partial class MapBase
{
public partial class MapBase
public static readonly DependencyProperty ForegroundProperty =
DependencyPropertyHelper.Register<MapBase, Brush>(nameof(Foreground),
new SolidColorBrush(Colors.Black));
public static readonly DependencyProperty AnimationEasingFunctionProperty =
DependencyPropertyHelper.Register<MapBase, EasingFunctionBase>(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));
public static readonly DependencyProperty TargetCenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(0d, 0d),
(map, oldValue, newValue) => map.TargetCenterPropertyChanged(newValue));
public static readonly DependencyProperty MinZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d,
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue));
public static readonly DependencyProperty MaxZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d,
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue));
public static readonly DependencyProperty ZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d,
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue));
public static readonly DependencyProperty TargetZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d,
(map, oldValue, newValue) => map.TargetZoomLevelPropertyChanged(newValue));
public static readonly DependencyProperty HeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d,
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue));
public static readonly DependencyProperty TargetHeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d,
(map, oldValue, newValue) => map.TargetHeadingPropertyChanged(newValue));
public static readonly DependencyProperty ViewScaleProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(ViewScale));
private static readonly DependencyProperty AnimatedCenterProperty =
DependencyPropertyHelper.Register<MapBase, Windows.Foundation.Point>(nameof(AnimatedCenter),
new Windows.Foundation.Point(),
(map, oldValue, newValue) => map.Center = new Location(newValue.Y, newValue.X));
private Windows.Foundation.Point AnimatedCenter => (Windows.Foundation.Point)GetValue(AnimatedCenterProperty);
private PointAnimation centerAnimation;
private DoubleAnimation zoomLevelAnimation;
private DoubleAnimation headingAnimation;
public MapBase()
{
public static readonly DependencyProperty ForegroundProperty =
DependencyPropertyHelper.Register<MapBase, Brush>(nameof(Foreground),
new SolidColorBrush(Colors.Black));
// Set Background by Style to enable resetting by ClearValue in MapLayerPropertyChanged.
// There is no default Style in Generic.xaml because MapBase has no DefaultStyleKey property.
//
var style = new Style(typeof(MapBase));
style.Setters.Add(new Setter(BackgroundProperty, new SolidColorBrush(Colors.White)));
style.Seal();
Style = style;
public static readonly DependencyProperty AnimationEasingFunctionProperty =
DependencyPropertyHelper.Register<MapBase, EasingFunctionBase>(nameof(AnimationEasingFunction),
new QuadraticEase { EasingMode = EasingMode.EaseOut });
SizeChanged += OnSizeChanged;
}
public static readonly DependencyProperty CenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(Center), new Location(0d, 0d),
(map, oldValue, newValue) => map.CenterPropertyChanged(newValue));
/// <summary>
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
/// The default value is a QuadraticEase with EasingMode.EaseOut.
/// </summary>
public EasingFunctionBase AnimationEasingFunction
{
get => (EasingFunctionBase)GetValue(AnimationEasingFunctionProperty);
set => SetValue(AnimationEasingFunctionProperty, value);
}
public static readonly DependencyProperty TargetCenterProperty =
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(0d, 0d),
(map, oldValue, newValue) => map.TargetCenterPropertyChanged(newValue));
/// <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(ViewScaleProperty, value);
}
public static readonly DependencyProperty MinZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d,
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue));
public static readonly DependencyProperty MaxZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d,
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue));
public static readonly DependencyProperty ZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d,
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue));
public static readonly DependencyProperty TargetZoomLevelProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d,
(map, oldValue, newValue) => map.TargetZoomLevelPropertyChanged(newValue));
public static readonly DependencyProperty HeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d,
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue));
public static readonly DependencyProperty TargetHeadingProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d,
(map, oldValue, newValue) => map.TargetHeadingPropertyChanged(newValue));
public static readonly DependencyProperty ViewScaleProperty =
DependencyPropertyHelper.Register<MapBase, double>(nameof(ViewScale));
private static readonly DependencyProperty AnimatedCenterProperty =
DependencyPropertyHelper.Register<MapBase, Windows.Foundation.Point>(nameof(AnimatedCenter),
new Windows.Foundation.Point(),
(map, oldValue, newValue) => map.Center = new Location(newValue.Y, newValue.X));
private Windows.Foundation.Point AnimatedCenter => (Windows.Foundation.Point)GetValue(AnimatedCenterProperty);
private PointAnimation centerAnimation;
private DoubleAnimation zoomLevelAnimation;
private DoubleAnimation headingAnimation;
public MapBase()
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
Clip = new RectangleGeometry
{
// Set Background by Style to enable resetting by ClearValue in MapLayerPropertyChanged.
// There is no default Style in Generic.xaml because MapBase has no DefaultStyleKey property.
//
var style = new Style(typeof(MapBase));
style.Setters.Add(new Setter(BackgroundProperty, new SolidColorBrush(Colors.White)));
style.Seal();
Style = style;
Rect = new Windows.Foundation.Rect(0d, 0d, e.NewSize.Width, e.NewSize.Height)
};
SizeChanged += OnSizeChanged;
}
ResetTransformCenter();
UpdateTransform();
}
/// <summary>
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
/// The default value is a QuadraticEase with EasingMode.EaseOut.
/// </summary>
public EasingFunctionBase AnimationEasingFunction
private void CenterPropertyChanged(Location value)
{
if (!internalPropertyChange)
{
get => (EasingFunctionBase)GetValue(AnimationEasingFunctionProperty);
set => SetValue(AnimationEasingFunctionProperty, value);
}
var center = CoerceCenterProperty(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(ViewScaleProperty, value);
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
Clip = new RectangleGeometry
if (!center.Equals(value))
{
Rect = new Windows.Foundation.Rect(0d, 0d, e.NewSize.Width, e.NewSize.Height)
};
SetValueInternal(CenterProperty, center);
}
ResetTransformCenter();
UpdateTransform();
}
private void CenterPropertyChanged(Location value)
{
if (!internalPropertyChange)
if (centerAnimation == null)
{
var center = CoerceCenterProperty(value);
if (!center.Equals(value))
{
SetValueInternal(CenterProperty, center);
}
UpdateTransform();
if (centerAnimation == null)
{
SetValueInternal(TargetCenterProperty, center);
}
SetValueInternal(TargetCenterProperty, center);
}
}
private void TargetCenterPropertyChanged(Location value)
{
if (!internalPropertyChange)
{
ResetTransformCenter();
var targetCenter = CoerceCenterProperty(value);
if (!targetCenter.Equals(value))
{
SetValueInternal(TargetCenterProperty, targetCenter);
}
if (!targetCenter.Equals(Center))
{
if (centerAnimation != null)
{
centerAnimation.Completed -= CenterAnimationCompleted;
}
centerAnimation = new PointAnimation
{
From = new Windows.Foundation.Point(Center.Longitude, Center.Latitude),
To = new Windows.Foundation.Point(NearestLongitude(targetCenter.Longitude), targetCenter.Latitude),
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction,
EnableDependentAnimation = true
};
centerAnimation.Completed += CenterAnimationCompleted;
BeginAnimation(nameof(AnimatedCenter), centerAnimation);
}
}
}
private void CenterAnimationCompleted(object sender, object e)
{
if (centerAnimation != null)
{
centerAnimation.Completed -= CenterAnimationCompleted;
centerAnimation = null;
SetValueInternal(CenterProperty, TargetCenter);
UpdateTransform();
}
}
private void MinZoomLevelPropertyChanged(double value)
{
var minZoomLevel = CoerceMinZoomLevelProperty(value);
if (minZoomLevel != value)
{
SetValueInternal(MinZoomLevelProperty, minZoomLevel);
}
if (ZoomLevel < minZoomLevel)
{
ZoomLevel = minZoomLevel;
}
}
private void MaxZoomLevelPropertyChanged(double value)
{
var maxZoomLevel = CoerceMaxZoomLevelProperty(value);
if (maxZoomLevel != value)
{
SetValueInternal(MaxZoomLevelProperty, maxZoomLevel);
}
if (ZoomLevel > maxZoomLevel)
{
ZoomLevel = maxZoomLevel;
}
}
private void ZoomLevelPropertyChanged(double value)
{
if (!internalPropertyChange)
{
var zoomLevel = CoerceZoomLevelProperty(value);
if (zoomLevel != value)
{
SetValueInternal(ZoomLevelProperty, zoomLevel);
}
UpdateTransform();
if (zoomLevelAnimation == null)
{
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
}
}
}
private void TargetZoomLevelPropertyChanged(double value)
{
if (!internalPropertyChange)
{
var targetZoomLevel = CoerceZoomLevelProperty(value);
if (targetZoomLevel != value)
{
SetValueInternal(TargetZoomLevelProperty, targetZoomLevel);
}
if (targetZoomLevel != ZoomLevel)
{
if (zoomLevelAnimation != null)
{
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
}
zoomLevelAnimation = new DoubleAnimation
{
To = targetZoomLevel,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction,
EnableDependentAnimation = true
};
zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted;
BeginAnimation(nameof(ZoomLevel), zoomLevelAnimation);
}
}
}
private void ZoomLevelAnimationCompleted(object sender, object e)
{
if (zoomLevelAnimation != null)
{
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
zoomLevelAnimation = null;
SetValueInternal(ZoomLevelProperty, TargetZoomLevel);
UpdateTransform(true);
}
}
private void HeadingPropertyChanged(double value)
{
if (!internalPropertyChange)
{
var heading = CoerceHeadingProperty(value);
if (heading != value)
{
SetValueInternal(HeadingProperty, heading);
}
UpdateTransform();
if (headingAnimation == null)
{
SetValueInternal(TargetHeadingProperty, heading);
}
}
}
private void TargetHeadingPropertyChanged(double value)
{
if (!internalPropertyChange)
{
var targetHeading = CoerceHeadingProperty(value);
if (targetHeading != value)
{
SetValueInternal(TargetHeadingProperty, targetHeading);
}
if (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,
EnableDependentAnimation = true
};
headingAnimation.Completed += HeadingAnimationCompleted;
BeginAnimation(nameof(Heading), headingAnimation);
}
}
}
private void HeadingAnimationCompleted(object sender, object e)
{
if (headingAnimation != null)
{
headingAnimation.Completed -= HeadingAnimationCompleted;
headingAnimation = null;
SetValueInternal(HeadingProperty, TargetHeading);
UpdateTransform();
}
}
private void BeginAnimation(string property, Timeline animation)
{
Storyboard.SetTarget(animation, this);
Storyboard.SetTargetProperty(animation, property);
var storyboard = new Storyboard();
storyboard.Children.Add(animation);
storyboard.Begin();
}
}
private void TargetCenterPropertyChanged(Location value)
{
if (!internalPropertyChange)
{
ResetTransformCenter();
var targetCenter = CoerceCenterProperty(value);
if (!targetCenter.Equals(value))
{
SetValueInternal(TargetCenterProperty, targetCenter);
}
if (!targetCenter.Equals(Center))
{
if (centerAnimation != null)
{
centerAnimation.Completed -= CenterAnimationCompleted;
}
centerAnimation = new PointAnimation
{
From = new Windows.Foundation.Point(Center.Longitude, Center.Latitude),
To = new Windows.Foundation.Point(NearestLongitude(targetCenter.Longitude), targetCenter.Latitude),
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction,
EnableDependentAnimation = true
};
centerAnimation.Completed += CenterAnimationCompleted;
BeginAnimation(nameof(AnimatedCenter), centerAnimation);
}
}
}
private void CenterAnimationCompleted(object sender, object e)
{
if (centerAnimation != null)
{
centerAnimation.Completed -= CenterAnimationCompleted;
centerAnimation = null;
SetValueInternal(CenterProperty, TargetCenter);
UpdateTransform();
}
}
private void MinZoomLevelPropertyChanged(double value)
{
var minZoomLevel = CoerceMinZoomLevelProperty(value);
if (minZoomLevel != value)
{
SetValueInternal(MinZoomLevelProperty, minZoomLevel);
}
if (ZoomLevel < minZoomLevel)
{
ZoomLevel = minZoomLevel;
}
}
private void MaxZoomLevelPropertyChanged(double value)
{
var maxZoomLevel = CoerceMaxZoomLevelProperty(value);
if (maxZoomLevel != value)
{
SetValueInternal(MaxZoomLevelProperty, maxZoomLevel);
}
if (ZoomLevel > maxZoomLevel)
{
ZoomLevel = maxZoomLevel;
}
}
private void ZoomLevelPropertyChanged(double value)
{
if (!internalPropertyChange)
{
var zoomLevel = CoerceZoomLevelProperty(value);
if (zoomLevel != value)
{
SetValueInternal(ZoomLevelProperty, zoomLevel);
}
UpdateTransform();
if (zoomLevelAnimation == null)
{
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
}
}
}
private void TargetZoomLevelPropertyChanged(double value)
{
if (!internalPropertyChange)
{
var targetZoomLevel = CoerceZoomLevelProperty(value);
if (targetZoomLevel != value)
{
SetValueInternal(TargetZoomLevelProperty, targetZoomLevel);
}
if (targetZoomLevel != ZoomLevel)
{
if (zoomLevelAnimation != null)
{
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
}
zoomLevelAnimation = new DoubleAnimation
{
To = targetZoomLevel,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction,
EnableDependentAnimation = true
};
zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted;
BeginAnimation(nameof(ZoomLevel), zoomLevelAnimation);
}
}
}
private void ZoomLevelAnimationCompleted(object sender, object e)
{
if (zoomLevelAnimation != null)
{
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
zoomLevelAnimation = null;
SetValueInternal(ZoomLevelProperty, TargetZoomLevel);
UpdateTransform(true);
}
}
private void HeadingPropertyChanged(double value)
{
if (!internalPropertyChange)
{
var heading = CoerceHeadingProperty(value);
if (heading != value)
{
SetValueInternal(HeadingProperty, heading);
}
UpdateTransform();
if (headingAnimation == null)
{
SetValueInternal(TargetHeadingProperty, heading);
}
}
}
private void TargetHeadingPropertyChanged(double value)
{
if (!internalPropertyChange)
{
var targetHeading = CoerceHeadingProperty(value);
if (targetHeading != value)
{
SetValueInternal(TargetHeadingProperty, targetHeading);
}
if (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,
EnableDependentAnimation = true
};
headingAnimation.Completed += HeadingAnimationCompleted;
BeginAnimation(nameof(Heading), headingAnimation);
}
}
}
private void HeadingAnimationCompleted(object sender, object e)
{
if (headingAnimation != null)
{
headingAnimation.Completed -= HeadingAnimationCompleted;
headingAnimation = null;
SetValueInternal(HeadingProperty, TargetHeading);
UpdateTransform();
}
}
private void BeginAnimation(string property, Timeline animation)
{
Storyboard.SetTarget(animation, this);
Storyboard.SetTargetProperty(animation, property);
var storyboard = new Storyboard();
storyboard.Children.Add(animation);
storyboard.Begin();
}
}

View file

@ -6,50 +6,49 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
#endif
namespace MapControl
namespace MapControl;
public partial class MapContentControl
{
public partial class MapContentControl
public MapContentControl()
{
public MapContentControl()
DefaultStyleKey = typeof(MapContentControl);
MapPanel.InitMapElement(this);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
var parentMap = MapPanel.GetParentMap(this);
if (parentMap != null)
{
DefaultStyleKey = typeof(MapContentControl);
MapPanel.InitMapElement(this);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
var parentMap = MapPanel.GetParentMap(this);
if (parentMap != null)
// Workaround for missing RelativeSource AncestorType=MapBase Bindings in default Style.
//
if (Background == null)
{
// Workaround for missing RelativeSource AncestorType=MapBase Bindings in default Style.
//
if (Background == null)
{
SetBinding(BackgroundProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Background)) });
}
if (Foreground == null)
{
SetBinding(ForegroundProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Foreground)) });
}
if (BorderBrush == null)
{
SetBinding(BorderBrushProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Foreground)) });
}
SetBinding(BackgroundProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Background)) });
}
if (Foreground == null)
{
SetBinding(ForegroundProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Foreground)) });
}
if (BorderBrush == null)
{
SetBinding(BorderBrushProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Foreground)) });
}
}
}
}
public partial class Pushpin
public partial class Pushpin
{
public Pushpin()
{
public Pushpin()
{
DefaultStyleKey = typeof(Pushpin);
}
DefaultStyleKey = typeof(Pushpin);
}
}

View file

@ -14,129 +14,128 @@ using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Shapes;
#endif
namespace MapControl
namespace MapControl;
public partial class MapGrid : MapPanel
{
public partial class MapGrid : MapPanel
public static readonly DependencyProperty ForegroundProperty =
DependencyPropertyHelper.Register<MapGrid, Brush>(nameof(Foreground));
public static readonly DependencyProperty FontFamilyProperty =
DependencyPropertyHelper.Register<MapGrid, FontFamily>(nameof(FontFamily));
public static readonly DependencyProperty FontSizeProperty =
DependencyPropertyHelper.Register<MapGrid, double>(nameof(FontSize), 12d);
protected override void SetParentMap(MapBase map)
{
public static readonly DependencyProperty ForegroundProperty =
DependencyPropertyHelper.Register<MapGrid, Brush>(nameof(Foreground));
public static readonly DependencyProperty FontFamilyProperty =
DependencyPropertyHelper.Register<MapGrid, FontFamily>(nameof(FontFamily));
public static readonly DependencyProperty FontSizeProperty =
DependencyPropertyHelper.Register<MapGrid, double>(nameof(FontSize), 12d);
protected override void SetParentMap(MapBase map)
if (map != null && Foreground == null)
{
if (map != null && Foreground == null)
{
SetBinding(ForegroundProperty,
new Binding { Source = map, Path = new PropertyPath(nameof(Foreground)) });
}
base.SetParentMap(map);
SetBinding(ForegroundProperty,
new Binding { Source = map, Path = new PropertyPath(nameof(Foreground)) });
}
protected override void OnViewportChanged(ViewportChangedEventArgs e)
base.SetParentMap(map);
}
protected override void OnViewportChanged(ViewportChangedEventArgs e)
{
Path path;
if (Children.Count == 0)
{
Path path;
path = new Path { Data = new PathGeometry() };
if (Children.Count == 0)
path.SetBinding(Shape.StrokeProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(Foreground)) });
path.SetBinding(Shape.StrokeThicknessProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(StrokeThickness)) });
Children.Add(path);
}
else
{
path = (Path)Children[0];
}
var childrenCount = 1;
var labels = new List<Label>();
var figures = ((PathGeometry)path.Data).Figures;
figures.Clear();
DrawGrid(figures, labels);
foreach (var label in labels)
{
TextBlock textBlock;
if (childrenCount < Children.Count)
{
path = new Path { Data = new PathGeometry() };
path.SetBinding(Shape.StrokeProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(Foreground)) });
path.SetBinding(Shape.StrokeThicknessProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(StrokeThickness)) });
Children.Add(path);
textBlock = (TextBlock)Children[childrenCount];
}
else
{
path = (Path)Children[0];
}
textBlock = new TextBlock { RenderTransform = new MatrixTransform() };
var childrenCount = 1;
var labels = new List<Label>();
var figures = ((PathGeometry)path.Data).Figures;
figures.Clear();
textBlock.SetBinding(TextBlock.FontSizeProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(FontSize)) });
DrawGrid(figures, labels);
textBlock.SetBinding(TextBlock.ForegroundProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(Foreground)) });
foreach (var label in labels)
{
TextBlock textBlock;
if (childrenCount < Children.Count)
if (FontFamily != null)
{
textBlock = (TextBlock)Children[childrenCount];
}
else
{
textBlock = new TextBlock { RenderTransform = new MatrixTransform() };
textBlock.SetBinding(TextBlock.FontSizeProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(FontSize)) });
textBlock.SetBinding(TextBlock.ForegroundProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(Foreground)) });
if (FontFamily != null)
{
textBlock.SetBinding(TextBlock.FontFamilyProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(FontFamily)) });
}
Children.Add(textBlock);
textBlock.SetBinding(TextBlock.FontFamilyProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(FontFamily)) });
}
textBlock.Text = label.Text;
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var x = label.HorizontalAlignment switch
{
HorizontalAlignment.Left => 2d,
HorizontalAlignment.Right => -textBlock.DesiredSize.Width - 2d,
_ => -textBlock.DesiredSize.Width / 2d
};
var y = label.VerticalAlignment switch
{
VerticalAlignment.Top => 0d,
VerticalAlignment.Bottom => -textBlock.DesiredSize.Height,
_ => -textBlock.DesiredSize.Height / 2d,
};
var matrix = new Matrix(1, 0, 0, 1, 0, 0);
matrix.Translate(x, y);
matrix.Rotate(label.Rotation);
matrix.Translate(label.X, label.Y);
((MatrixTransform)textBlock.RenderTransform).Matrix = matrix;
childrenCount++;
Children.Add(textBlock);
}
while (Children.Count > childrenCount)
textBlock.Text = label.Text;
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var x = label.HorizontalAlignment switch
{
Children.RemoveAt(Children.Count - 1);
}
HorizontalAlignment.Left => 2d,
HorizontalAlignment.Right => -textBlock.DesiredSize.Width - 2d,
_ => -textBlock.DesiredSize.Width / 2d
};
var y = label.VerticalAlignment switch
{
VerticalAlignment.Top => 0d,
VerticalAlignment.Bottom => -textBlock.DesiredSize.Height,
_ => -textBlock.DesiredSize.Height / 2d,
};
base.OnViewportChanged(e);
var matrix = new Matrix(1, 0, 0, 1, 0, 0);
matrix.Translate(x, y);
matrix.Rotate(label.Rotation);
matrix.Translate(label.X, label.Y);
((MatrixTransform)textBlock.RenderTransform).Matrix = matrix;
childrenCount++;
}
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
while (Children.Count > childrenCount)
{
var polyline = new PolyLineSegment();
foreach (var p in points)
{
polyline.Points.Add(p);
}
return polyline;
Children.RemoveAt(Children.Count - 1);
}
base.OnViewportChanged(e);
}
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
{
var polyline = new PolyLineSegment();
foreach (var p in points)
{
polyline.Points.Add(p);
}
return polyline;
}
}

View file

@ -5,35 +5,34 @@ using Windows.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Media.Animation;
#endif
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, nameof(Opacity));
Storyboard.SetTarget(fadeInAnimation, Children[1]);
Storyboard.SetTargetProperty(fadeInAnimation, nameof(Opacity));
Storyboard.SetTarget(fadeOutAnimation, Children[0]);
Storyboard.SetTargetProperty(fadeOutAnimation, nameof(Opacity));
Storyboard.SetTarget(fadeOutAnimation, Children[0]);
Storyboard.SetTargetProperty(fadeOutAnimation, nameof(Opacity));
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

@ -12,80 +12,79 @@ using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
#endif
namespace MapControl
namespace MapControl;
public partial class MapItem
{
public partial class MapItem
private Windows.Foundation.Point? pointerPressedPosition;
public MapItem()
{
private Windows.Foundation.Point? pointerPressedPosition;
DefaultStyleKey = typeof(MapItem);
MapPanel.InitMapElement(this);
}
public MapItem()
{
DefaultStyleKey = typeof(MapItem);
MapPanel.InitMapElement(this);
}
protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
base.OnPointerPressed(e);
pointerPressedPosition = e.GetCurrentPoint(null).Position;
e.Handled = true;
}
protected override void OnPointerPressed(PointerRoutedEventArgs e)
protected override void OnPointerReleased(PointerRoutedEventArgs e)
{
if (pointerPressedPosition.HasValue)
{
base.OnPointerPressed(e);
pointerPressedPosition = e.GetCurrentPoint(null).Position;
e.Handled = true;
}
const float pointerMovementThreshold = 2f;
var p = e.GetCurrentPoint(null).Position;
protected override void OnPointerReleased(PointerRoutedEventArgs e)
{
if (pointerPressedPosition.HasValue)
// Perform selection only when no significant pointer movement occured.
//
if (Math.Abs(p.X - pointerPressedPosition.Value.X) <= pointerMovementThreshold &&
Math.Abs(p.Y - pointerPressedPosition.Value.Y) <= pointerMovementThreshold &&
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl)
{
const float pointerMovementThreshold = 2f;
var p = e.GetCurrentPoint(null).Position;
// Perform selection only when no significant pointer movement occured.
//
if (Math.Abs(p.X - pointerPressedPosition.Value.X) <= pointerMovementThreshold &&
Math.Abs(p.Y - pointerPressedPosition.Value.Y) <= pointerMovementThreshold &&
ItemsControl.ItemsControlFromItemContainer(this) is MapItemsControl mapItemsControl)
if (mapItemsControl.SelectionMode == SelectionMode.Extended &&
e.KeyModifiers.HasFlag(VirtualKeyModifiers.Shift))
{
if (mapItemsControl.SelectionMode == SelectionMode.Extended &&
e.KeyModifiers.HasFlag(VirtualKeyModifiers.Shift))
{
mapItemsControl.SelectItemsInRange(this);
}
else
{
base.OnPointerReleased(e);
}
mapItemsControl.SelectItemsInRange(this);
}
else
{
base.OnPointerReleased(e);
}
pointerPressedPosition = null;
}
e.Handled = true;
pointerPressedPosition = null;
}
protected override void OnApplyTemplate()
e.Handled = true;
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
var parentMap = MapPanel.GetParentMap(this);
if (parentMap != null)
{
base.OnApplyTemplate();
var parentMap = MapPanel.GetParentMap(this);
if (parentMap != null)
// Workaround for missing RelativeSource AncestorType=MapBase Bindings in default Style.
//
if (Background == null)
{
// Workaround for missing RelativeSource AncestorType=MapBase Bindings in default Style.
//
if (Background == null)
{
SetBinding(BackgroundProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Background)) });
}
if (Foreground == null)
{
SetBinding(ForegroundProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Foreground)) });
}
if (BorderBrush == null)
{
SetBinding(BorderBrushProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Foreground)) });
}
SetBinding(BackgroundProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Background)) });
}
if (Foreground == null)
{
SetBinding(ForegroundProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Foreground)) });
}
if (BorderBrush == null)
{
SetBinding(BorderBrushProperty,
new Binding { Source = parentMap, Path = new PropertyPath(nameof(Foreground)) });
}
}
}

View file

@ -4,41 +4,40 @@ using Windows.UI.Xaml;
using Microsoft.UI.Xaml;
#endif
namespace MapControl
namespace MapControl;
public partial class MapItemsControl
{
public partial class MapItemsControl
public MapItemsControl()
{
public MapItemsControl()
{
DefaultStyleKey = typeof(MapItemsControl);
MapPanel.InitMapElement(this);
}
DefaultStyleKey = typeof(MapItemsControl);
MapPanel.InitMapElement(this);
}
public new MapItem ContainerFromItem(object item)
{
return (MapItem)base.ContainerFromItem(item);
}
public new MapItem ContainerFromItem(object item)
{
return (MapItem)base.ContainerFromItem(item);
}
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

@ -6,52 +6,51 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
#endif
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,
(element, oldValue, newValue) => (element.Parent as MapPanel)?.InvalidateArrange());
public static readonly DependencyProperty BoundingBoxProperty =
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel), null,
(element, oldValue, newValue) => (element.Parent as MapPanel)?.InvalidateArrange());
public static readonly DependencyProperty MapRectProperty =
DependencyPropertyHelper.RegisterAttached<Rect?>("MapRect", typeof(MapPanel), null,
(element, oldValue, newValue) => (element.Parent as MapPanel)?.InvalidateArrange());
public static void InitMapElement(FrameworkElement element)
{
public static readonly DependencyProperty AutoCollapseProperty =
DependencyPropertyHelper.RegisterAttached<bool>("AutoCollapse", typeof(MapPanel));
// Workaround for missing property value inheritance.
// Loaded and Unloaded handlers set and clear the ParentMap property value.
//
element.Loaded += (_, _) => GetParentMap(element);
element.Unloaded += (_, _) => element.ClearValue(ParentMapProperty);
}
public static readonly DependencyProperty LocationProperty =
DependencyPropertyHelper.RegisterAttached<Location>("Location", typeof(MapPanel), null,
(element, oldValue, newValue) => (element.Parent as MapPanel)?.InvalidateArrange());
public static MapBase GetParentMap(FrameworkElement element)
{
var parentMap = (MapBase)element.GetValue(ParentMapProperty);
public static readonly DependencyProperty BoundingBoxProperty =
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel), null,
(element, oldValue, newValue) => (element.Parent as MapPanel)?.InvalidateArrange());
public static readonly DependencyProperty MapRectProperty =
DependencyPropertyHelper.RegisterAttached<Rect?>("MapRect", typeof(MapPanel), null,
(element, oldValue, newValue) => (element.Parent as MapPanel)?.InvalidateArrange());
public static void InitMapElement(FrameworkElement element)
// Traverse visual tree because of missing property value inheritance.
//
if (parentMap == null &&
VisualTreeHelper.GetParent(element) is FrameworkElement parentElement)
{
// Workaround for missing property value inheritance.
// Loaded and Unloaded handlers set and clear the ParentMap property value.
//
element.Loaded += (_, _) => GetParentMap(element);
element.Unloaded += (_, _) => element.ClearValue(ParentMapProperty);
}
parentMap = (parentElement as MapBase) ?? GetParentMap(parentElement);
public static MapBase GetParentMap(FrameworkElement element)
{
var parentMap = (MapBase)element.GetValue(ParentMapProperty);
// Traverse visual tree because of missing property value inheritance.
//
if (parentMap == null &&
VisualTreeHelper.GetParent(element) is FrameworkElement parentElement)
if (parentMap != null)
{
parentMap = (parentElement as MapBase) ?? GetParentMap(parentElement);
if (parentMap != null)
{
element.SetValue(ParentMapProperty, parentMap);
}
element.SetValue(ParentMapProperty, parentMap);
}
return parentMap;
}
return parentMap;
}
}

View file

@ -4,13 +4,12 @@ using Windows.UI.Xaml.Shapes;
using Microsoft.UI.Xaml.Shapes;
#endif
namespace MapControl
namespace MapControl;
public partial class MapPath : Path
{
public partial class MapPath : Path
public MapPath()
{
public MapPath()
{
MapPanel.InitMapElement(this);
}
MapPanel.InitMapElement(this);
}
}

View file

@ -7,74 +7,73 @@ using Windows.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media;
#endif
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 = ((PathGeometry)Data).Figures;
figures.Clear();
if (ParentMap != null && locations != null)
{
var figures = ((PathGeometry)Data).Figures;
figures.Clear();
var longitudeOffset = GetLongitudeOffset(locations);
if (ParentMap != null && locations != null)
AddPolylinePoints(figures, locations, longitudeOffset, closed);
}
}
protected void UpdateData(IEnumerable<IEnumerable<Location>> polygons)
{
var figures = ((PathGeometry)Data).Figures;
figures.Clear();
if (ParentMap != null && polygons != null)
{
var longitudeOffset = GetLongitudeOffset(polygons.FirstOrDefault());
foreach (var locations in polygons)
{
var longitudeOffset = GetLongitudeOffset(locations);
AddPolylinePoints(figures, locations, longitudeOffset, closed);
AddPolylinePoints(figures, locations, longitudeOffset, true);
}
}
}
protected void UpdateData(IEnumerable<IEnumerable<Location>> polygons)
private void AddPolylinePoints(PathFigureCollection figures, IEnumerable<Location> locations, double longitudeOffset, bool closed)
{
var points = locations.Select(location => LocationToView(location, longitudeOffset));
if (points.Any())
{
var figures = ((PathGeometry)Data).Figures;
figures.Clear();
var start = points.First();
var polyline = new PolyLineSegment();
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 points.Skip(1))
{
var longitudeOffset = GetLongitudeOffset(polygons.FirstOrDefault());
foreach (var locations in polygons)
{
AddPolylinePoints(figures, locations, longitudeOffset, true);
}
polyline.Points.Add(point);
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(PathFigureCollection figures, 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 = new PolyLineSegment();
var minX = start.X;
var maxX = start.X;
var minY = start.Y;
var maxY = start.Y;
foreach (var point in points.Skip(1))
var figure = new PathFigure
{
polyline.Points.Add(point);
minX = Math.Min(minX, point.X);
maxX = Math.Max(maxX, point.X);
minY = Math.Min(minY, point.Y);
maxY = Math.Max(maxY, point.Y);
}
StartPoint = start,
IsClosed = closed,
IsFilled = true
};
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);
}
figure.Segments.Add(polyline);
figures.Add(figure);
}
}
}

View file

@ -1,27 +1,26 @@
using System;
namespace MapControl
namespace MapControl;
/// <summary>
/// Replaces Windows.Foundation.Point for double floating point precision.
/// </summary>
public readonly struct Point(double x, double y) : IEquatable<Point>
{
/// <summary>
/// Replaces Windows.Foundation.Point for double floating point precision.
/// </summary>
public readonly struct Point(double x, double y) : IEquatable<Point>
{
public double X => x;
public double Y => y;
public double X => x;
public double Y => y;
public static implicit operator Windows.Foundation.Point(Point p) => new(p.X, p.Y);
public static implicit operator Windows.Foundation.Point(Point p) => new(p.X, p.Y);
public static implicit operator Point(Windows.Foundation.Point p) => new(p.X, p.Y);
public static implicit operator Point(Windows.Foundation.Point p) => new(p.X, p.Y);
public static bool operator ==(Point p1, Point p2) => p1.Equals(p2);
public static bool operator ==(Point p1, Point p2) => p1.Equals(p2);
public static bool operator !=(Point p1, Point p2) => !p1.Equals(p2);
public static bool operator !=(Point p1, Point p2) => !p1.Equals(p2);
public bool Equals(Point p) => X == p.X && Y == p.Y;
public bool Equals(Point p) => X == p.X && Y == p.Y;
public override bool Equals(object obj) => obj is Point p && Equals(p);
public override bool Equals(object obj) => obj is Point p && Equals(p);
public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
}
public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
}

View file

@ -15,63 +15,62 @@ using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Shapes;
#endif
namespace MapControl
namespace MapControl;
[ContentProperty(Name = "Child")]
public partial class PushpinBorder : UserControl
{
[ContentProperty(Name = "Child")]
public partial class PushpinBorder : UserControl
public static readonly DependencyProperty ArrowSizeProperty =
DependencyPropertyHelper.Register<PushpinBorder, Size>(nameof(ArrowSize), new Size(10d, 20d),
(border, oldValue, newValue) => border.SetBorderMargin());
public static readonly DependencyProperty BorderWidthProperty =
DependencyPropertyHelper.Register<PushpinBorder, double>(nameof(BorderWidth), 0d,
(border, oldValue, newValue) => border.SetBorderMargin());
private readonly Border border = new Border();
public PushpinBorder()
{
public static readonly DependencyProperty ArrowSizeProperty =
DependencyPropertyHelper.Register<PushpinBorder, Size>(nameof(ArrowSize), new Size(10d, 20d),
(border, oldValue, newValue) => border.SetBorderMargin());
public static readonly DependencyProperty BorderWidthProperty =
DependencyPropertyHelper.Register<PushpinBorder, double>(nameof(BorderWidth), 0d,
(border, oldValue, newValue) => border.SetBorderMargin());
private readonly Border border = new Border();
public PushpinBorder()
var path = new Path
{
var path = new Path
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Stretch = Stretch.None
};
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Stretch = Stretch.None
};
path.SetBinding(Shape.FillProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(Background)) });
path.SetBinding(Shape.FillProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(Background)) });
path.SetBinding(Shape.StrokeProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(BorderBrush)) });
path.SetBinding(Shape.StrokeProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(BorderBrush)) });
path.SetBinding(Shape.StrokeThicknessProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(BorderWidth)) });
path.SetBinding(Shape.StrokeThicknessProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(BorderWidth)) });
border.SetBinding(PaddingProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(Padding)) });
border.SetBinding(PaddingProperty,
new Binding { Source = this, Path = new PropertyPath(nameof(Padding)) });
SetBorderMargin();
SetBorderMargin();
var grid = new Grid();
grid.Children.Add(path);
grid.Children.Add(border);
var grid = new Grid();
grid.Children.Add(path);
grid.Children.Add(border);
Content = grid;
Content = grid;
SizeChanged += (_, _) => path.Data = BuildGeometry();
}
SizeChanged += (_, _) => path.Data = BuildGeometry();
}
public UIElement Child
{
get => border.Child;
set => border.Child = value;
}
public UIElement Child
{
get => border.Child;
set => border.Child = value;
}
private void SetBorderMargin()
{
border.Margin = new Thickness(
BorderWidth, BorderWidth, BorderWidth, BorderWidth + ArrowSize.Height);
}
private void SetBorderMargin()
{
border.Margin = new Thickness(
BorderWidth, BorderWidth, BorderWidth, BorderWidth + ArrowSize.Height);
}
}

View file

@ -1,31 +1,30 @@
using System;
namespace MapControl
namespace MapControl;
/// <summary>
/// Replaces Windows.Foundation.Rect for double floating point precision.
/// </summary>
public readonly struct Rect(double x, double y, double width, double height) : IEquatable<Rect>
{
/// <summary>
/// Replaces Windows.Foundation.Rect for double floating point precision.
/// </summary>
public readonly struct Rect(double x, double y, double width, double height) : IEquatable<Rect>
{
public double X => x;
public double Y => y;
public double Width => width;
public double Height => height;
public double X => x;
public double Y => y;
public double Width => width;
public double Height => height;
public static implicit operator Windows.Foundation.Rect(Rect r) => new(r.X, r.Y, r.Width, r.Height);
public static implicit operator Windows.Foundation.Rect(Rect r) => new(r.X, r.Y, r.Width, r.Height);
public static implicit operator Rect(Windows.Foundation.Rect r) => new(r.X, r.Y, r.Width, r.Height);
public static implicit operator Rect(Windows.Foundation.Rect r) => new(r.X, r.Y, r.Width, r.Height);
public static bool operator ==(Rect r1, Rect r2) => r1.Equals(r2);
public static bool operator ==(Rect r1, Rect r2) => r1.Equals(r2);
public static bool operator !=(Rect r1, Rect r2) => !r1.Equals(r2);
public static bool operator !=(Rect r1, Rect r2) => !r1.Equals(r2);
public bool Equals(Rect r) => X == r.X && Y == r.Y && Width == r.Width && Height == r.Height;
public bool Equals(Rect r) => X == r.X && Y == r.Y && Width == r.Width && Height == r.Height;
public override bool Equals(object obj) => obj is Rect r && Equals(r);
public override bool Equals(object obj) => obj is Rect r && Equals(r);
public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode() ^ Width.GetHashCode() ^ Height.GetHashCode();
public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode() ^ Width.GetHashCode() ^ Height.GetHashCode();
public bool Contains(Point p) => p.X >= X && p.X <= X + Width && p.Y >= Y && p.Y <= Y + Height;
}
public bool Contains(Point p) => p.X >= X && p.X <= X + Width && p.Y >= Y && p.Y <= Y + Height;
}