// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control // Copyright © 2024 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) using System; using System.Linq; #if WINUI using Windows.Foundation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; #elif UWP using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; #else using System.Windows; using System.Windows.Controls; using System.Windows.Media; #endif namespace MapControl { /// /// Optional interface to hold the value of the attached property MapPanel.ParentMap. /// public interface IMapElement { MapBase ParentMap { get; set; } } /// /// Arranges child elements on a Map at positions specified by the attached property Location, /// or in rectangles specified by the attached property BoundingBox. /// public partial class MapPanel : Panel, IMapElement { private static void ParentMapPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { if (obj is IMapElement mapElement) { mapElement.ParentMap = e.NewValue as MapBase; } } private MapBase parentMap; /// /// Implements IMapElement.ParentMap. /// public MapBase ParentMap { get => parentMap; set => SetParentMap(value); } public static readonly DependencyProperty AutoCollapseProperty = DependencyProperty.RegisterAttached( "AutoCollapse", typeof(bool), typeof(MapPanel), new PropertyMetadata(false)); /// /// Gets a value that controls whether an element's Visibility is automatically /// set to Collapsed when it is located outside the visible viewport area. /// public static bool GetAutoCollapse(FrameworkElement element) { return (bool)element.GetValue(AutoCollapseProperty); } /// /// Sets the AutoCollapse property. /// public static void SetAutoCollapse(FrameworkElement element, bool value) { element.SetValue(AutoCollapseProperty, value); } /// /// Gets the geodetic Location of an element. /// public static Location GetLocation(FrameworkElement element) { return (Location)element.GetValue(LocationProperty); } /// /// Sets the geodetic Location of an element. /// public static void SetLocation(FrameworkElement element, Location value) { element.SetValue(LocationProperty, value); } /// /// Gets the BoundingBox of an element. /// public static BoundingBox GetBoundingBox(FrameworkElement element) { return (BoundingBox)element.GetValue(BoundingBoxProperty); } /// /// Sets the BoundingBox of an element. /// public static void SetBoundingBox(FrameworkElement element, BoundingBox value) { element.SetValue(BoundingBoxProperty, value); } /// /// Gets the view position of an element with Location. /// public static Point? GetViewPosition(FrameworkElement element) { return (Point?)element.GetValue(ViewPositionProperty); } 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.OfType()) { element.Measure(availableSize); } return new Size(); } protected override Size ArrangeOverride(Size finalSize) { if (parentMap != null) { foreach (var element in Children.OfType()) { var location = GetLocation(element); var position = location != null ? GetViewPosition(location) : null; SetViewPosition(element, ref position); if (GetAutoCollapse(element)) { element.Visibility = position.HasValue && IsOutsideViewport(position.Value) ? Visibility.Collapsed : Visibility.Visible; } if (position.HasValue) { ArrangeElement(element, position.Value); } else { var boundingBox = GetBoundingBox(element); if (boundingBox != null) { var viewRect = GetViewRect(boundingBox); if (viewRect != null) { ArrangeElement(element, viewRect); } } else { ArrangeElement(element, finalSize); } } } } return finalSize; } protected Point? GetViewPosition(Location location) { var position = parentMap.LocationToView(location); if (parentMap.MapProjection.Type <= MapProjectionType.NormalCylindrical && position.HasValue && IsOutsideViewport(position.Value)) { position = parentMap.LocationToView( new Location(location.Latitude, parentMap.ConstrainedLongitude(location.Longitude))); } return position; } protected ViewRect GetViewRect(BoundingBox boundingBox) { var mapRect = parentMap.MapProjection.BoundingBoxToMapRect(boundingBox); return mapRect != null ? GetViewRect(mapRect) : null; } protected ViewRect GetViewRect(MapRect mapRect) { var position = parentMap.ViewTransform.MapToView(mapRect.Center); if (parentMap.MapProjection.Type <= MapProjectionType.NormalCylindrical && IsOutsideViewport(position)) { var location = parentMap.MapProjection.MapToLocation(mapRect.Center); if (location != null) { location.Longitude = parentMap.ConstrainedLongitude(location.Longitude); var pos = parentMap.LocationToView(location); if (pos.HasValue) { position = pos.Value; } } } 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 ViewRect(x, y, width, height, parentMap.ViewTransform.Rotation); } private bool IsOutsideViewport(Point point) { return point.X < 0d || point.X > parentMap.RenderSize.Width || point.Y < 0d || point.Y > parentMap.RenderSize.Height; } private static void ArrangeElement(FrameworkElement element, Point position) { var size = GetDesiredSize(element); var rect = new Rect(position.X, position.Y, size.Width, size.Height); switch (element.HorizontalAlignment) { case HorizontalAlignment.Center: rect.X -= rect.Width / 2d; break; case HorizontalAlignment.Right: rect.X -= rect.Width; break; default: break; } switch (element.VerticalAlignment) { case VerticalAlignment.Center: rect.Y -= rect.Height / 2d; break; case VerticalAlignment.Bottom: rect.Y -= rect.Height; break; default: break; } ArrangeElement(element, rect); } private static void ArrangeElement(FrameworkElement element, Size parentSize) { var size = GetDesiredSize(element); var rect = new Rect(0d, 0d, size.Width, size.Height); switch (element.HorizontalAlignment) { case HorizontalAlignment.Center: rect.X = (parentSize.Width - rect.Width) / 2d; break; case HorizontalAlignment.Right: rect.X = parentSize.Width - rect.Width; break; case HorizontalAlignment.Stretch: rect.Width = parentSize.Width; break; default: break; } switch (element.VerticalAlignment) { case VerticalAlignment.Center: rect.Y = (parentSize.Height - rect.Height) / 2d; break; case VerticalAlignment.Bottom: rect.Y = parentSize.Height - rect.Height; break; case VerticalAlignment.Stretch: rect.Height = parentSize.Height; break; default: break; } ArrangeElement(element, rect); } private static void ArrangeElement(FrameworkElement element, ViewRect rect) { element.Width = rect.Width; element.Height = rect.Height; ArrangeElement(element, new Rect(rect.X, rect.Y, rect.Width, rect.Height)); if (element.RenderTransform is RotateTransform rotateTransform) { rotateTransform.Angle = rect.Rotation; } else if (rect.Rotation != 0d) { rotateTransform = new RotateTransform { Angle = rect.Rotation }; element.RenderTransform = rotateTransform; element.RenderTransformOrigin = new Point(0.5, 0.5); } } private static void ArrangeElement(FrameworkElement element, Rect rect) { if (element.UseLayoutRounding) { rect.X = Math.Round(rect.X); rect.Y = Math.Round(rect.Y); rect.Width = Math.Round(rect.Width); rect.Height = Math.Round(rect.Height); } element.Arrange(rect); } internal static Size GetDesiredSize(UIElement element) { var width = 0d; var height = 0d; if (element.DesiredSize.Width >= 0d && element.DesiredSize.Width < double.PositiveInfinity) { width = element.DesiredSize.Width; } if (element.DesiredSize.Height >= 0d && element.DesiredSize.Height < double.PositiveInfinity) { height = element.DesiredSize.Height; } return new Size(width, height); } } }