diff --git a/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs b/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs new file mode 100644 index 00000000..d4333b06 --- /dev/null +++ b/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs @@ -0,0 +1,47 @@ + +using Avalonia.Controls; +using System; + +namespace MapControl +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001")] + public static class DependencyPropertyHelper + { + public static AvaloniaProperty Register( + string name, + TValue defaultValue = default, + bool bindTwoWayByDefault = false, + Action propertyChanged = null) + where TOwner : AvaloniaObject + { + StyledProperty property = AvaloniaProperty.Register(name, defaultValue, false, + bindTwoWayByDefault ? Avalonia.Data.BindingMode.TwoWay : Avalonia.Data.BindingMode.OneWay); + + if (propertyChanged != null) + { + property.Changed.AddClassHandler( + (o, e) => propertyChanged(o, e.OldValue.Value, e.NewValue.Value)); + } + + return property; + } + + public static AvaloniaProperty RegisterAttached( + string name, + TValue defaultValue = default, + bool inherits = false, + Action propertyChanged = null) + where TOwner : AvaloniaObject + { + AttachedProperty property = AvaloniaProperty.RegisterAttached(name, defaultValue, inherits); + + if (propertyChanged != null) + { + property.Changed.AddClassHandler( + (o, e) => propertyChanged(o, e.OldValue.Value, e.NewValue.Value)); + } + + return property; + } + } +} diff --git a/MapControl/Avalonia/LocationAnimator.Avalonia.cs b/MapControl/Avalonia/LocationAnimator.Avalonia.cs new file mode 100644 index 00000000..03b7c96e --- /dev/null +++ b/MapControl/Avalonia/LocationAnimator.Avalonia.cs @@ -0,0 +1,16 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// Copyright © 2024 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using Avalonia.Animation; + +namespace MapControl +{ + public class LocationAnimator : InterpolatingAnimator + { + public override Location Interpolate(double progress, Location oldValue, Location newValue) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/MapControl/Avalonia/Map.Avalonia.cs b/MapControl/Avalonia/Map.Avalonia.cs index e37b5d23..0bd8aec6 100644 --- a/MapControl/Avalonia/Map.Avalonia.cs +++ b/MapControl/Avalonia/Map.Avalonia.cs @@ -3,7 +3,6 @@ // Licensed under the Microsoft Public License (Ms-PL) using Avalonia.Input; -using System.Threading; namespace MapControl { @@ -16,16 +15,6 @@ namespace MapControl = AvaloniaProperty.Register(nameof(MouseWheelZoomDelta), 0.25); private Point? mousePosition; - private double targetZoomLevel; - private CancellationTokenSource cancellationTokenSource; - - public Map() - { - PointerWheelChanged += OnPointerWheelChanged; - PointerPressed += OnPointerPressed; - PointerReleased += OnPointerReleased; - PointerMoved += OnPointerMoved; - } /// /// Gets or sets the amount by which the ZoomLevel property changes by a MouseWheel event. @@ -37,30 +26,17 @@ namespace MapControl set => SetValue(MouseWheelZoomDeltaProperty, value); } - private async void OnPointerWheelChanged(object sender, PointerWheelEventArgs e) + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { - var delta = MouseWheelZoomDelta * e.Delta.Y; + base.OnPointerWheelChanged(e); - if (cancellationTokenSource != null) - { - cancellationTokenSource.Cancel(); - targetZoomLevel += delta; - } - else - { - targetZoomLevel = ZoomLevel + delta; - } - - cancellationTokenSource = new CancellationTokenSource(); - - await ZoomMap(e.GetPosition(this), targetZoomLevel, cancellationTokenSource.Token); - - cancellationTokenSource.Dispose(); - cancellationTokenSource = null; + ZoomMap(e.GetPosition(this), TargetZoomLevel + MouseWheelZoomDelta * e.Delta.Y); } - private void OnPointerPressed(object sender, PointerPressedEventArgs e) + protected override void OnPointerPressed(PointerPressedEventArgs e) { + base.OnPointerPressed(e); + var point = e.GetCurrentPoint(this); if (point.Properties.IsLeftButtonPressed) @@ -70,8 +46,10 @@ namespace MapControl } } - private void OnPointerReleased(object sender, PointerReleasedEventArgs e) + protected override void OnPointerReleased(PointerReleasedEventArgs e) { + base.OnPointerReleased(e); + if (mousePosition.HasValue) { e.Pointer.Capture(null); @@ -79,8 +57,10 @@ namespace MapControl } } - private void OnPointerMoved(object sender, PointerEventArgs e) + protected override void OnPointerMoved(PointerEventArgs e) { + base.OnPointerMoved(e); + if (mousePosition.HasValue) { var position = e.GetPosition(this); diff --git a/MapControl/Avalonia/MapBase.Avalonia.cs b/MapControl/Avalonia/MapBase.Avalonia.cs index a3a873c3..02715b1a 100644 --- a/MapControl/Avalonia/MapBase.Avalonia.cs +++ b/MapControl/Avalonia/MapBase.Avalonia.cs @@ -6,44 +6,23 @@ global using Avalonia; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Data; -using Avalonia.Media; using Avalonia.Styling; using System; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace MapControl { - public interface IMapLayer + public partial class MapBase { - IBrush MapBackground { get; } - IBrush MapForeground { get; } - } - - public class MapBase : MapPanel - { - public static TimeSpan ImageFadeDuration { get; set; } = TimeSpan.FromSeconds(0.1); - - public static readonly StyledProperty ForegroundProperty - = AvaloniaProperty.Register(nameof(Foreground)); - - public static readonly StyledProperty AnimationDurationProperty - = AvaloniaProperty.Register(nameof(AnimationDuration), TimeSpan.FromSeconds(0.3)); - - public static readonly StyledProperty MapLayerProperty - = AvaloniaProperty.Register(nameof(MapLayer)); - - public static readonly StyledProperty MapProjectionProperty - = AvaloniaProperty.Register(nameof(MapProjection), new WebMercatorProjection()); - - public static readonly StyledProperty ProjectionCenterProperty - = AvaloniaProperty.Register(nameof(ProjectionCenter)); - public static readonly StyledProperty CenterProperty = AvaloniaProperty.Register(nameof(Center), new Location(), false, BindingMode.TwoWay, null, (map, center) => ((MapBase)map).CoerceCenterProperty(center)); + public static readonly StyledProperty TargetCenterProperty + = AvaloniaProperty.Register(nameof(TargetCenter), new Location(), false, + BindingMode.TwoWay, null, (map, center) => ((MapBase)map).CoerceCenterProperty(center)); + public static readonly StyledProperty MinZoomLevelProperty = AvaloniaProperty.Register(nameof(MinZoomLevel), 1d, false, BindingMode.OneWay, null, (map, minZoomLevel) => ((MapBase)map).CoerceMinZoomLevelProperty(minZoomLevel)); @@ -56,37 +35,46 @@ namespace MapControl = AvaloniaProperty.Register(nameof(ZoomLevel), 1d, false, BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel)); + public static readonly StyledProperty TargetZoomLevelProperty + = AvaloniaProperty.Register(nameof(TargetZoomLevel), 1d, false, + BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel)); + public static readonly StyledProperty HeadingProperty = AvaloniaProperty.Register(nameof(Heading), 0d, false, - BindingMode.TwoWay, null, (map, heading) => ((heading % 360d) + 360d) % 360d); + BindingMode.TwoWay, null, (map, heading) => CoerceHeadingProperty(heading)); + + public static readonly StyledProperty TargetHeadingProperty + = AvaloniaProperty.Register(nameof(TargetHeading), 0d, false, + BindingMode.TwoWay, null, (map, heading) => CoerceHeadingProperty(heading)); public static readonly DirectProperty ViewScaleProperty = AvaloniaProperty.RegisterDirect(nameof(ViewScale), map => map.ViewScale); - private Location transformCenter; - private Point viewCenter; - private double centerLongitude; - private double maxLatitude = 90d; + private CancellationTokenSource centerCts; + private CancellationTokenSource zoomLevelCts; + private CancellationTokenSource headingCts; + private Animation centerAnimation; + private Animation zoomLevelAnimation; + private Animation headingAnimation; static MapBase() { - MapLayerProperty.Changed.AddClassHandler( - (map, args) => map.MapLayerPropertyChanged(args)); - - MapProjectionProperty.Changed.AddClassHandler( - (map, args) => map.MapProjectionPropertyChanged(args.NewValue.Value)); - - ProjectionCenterProperty.Changed.AddClassHandler( - (map, args) => map.ProjectionCenterPropertyChanged()); + ClipToBoundsProperty.OverrideDefaultValue(typeof(MapBase), true); CenterProperty.Changed.AddClassHandler( - (map, args) => map.UpdateTransform()); + (map, args) => map.CenterPropertyChanged(args.NewValue.Value)); ZoomLevelProperty.Changed.AddClassHandler( - (map, args) => map.UpdateTransform()); + (map, args) => map.ZoomLevelPropertyChanged(args.NewValue.Value)); + + TargetZoomLevelProperty.Changed.AddClassHandler( + async (map, args) => await map.TargetZoomLevelPropertyChanged(args.NewValue.Value)); HeadingProperty.Changed.AddClassHandler( - (map, args) => map.UpdateTransform()); + (map, args) => map.HeadingPropertyChanged(args.NewValue.Value)); + + TargetHeadingProperty.Changed.AddClassHandler( + async (map, args) => await map.TargetHeadingPropertyChanged(args.NewValue.Value)); } public MapBase() @@ -94,105 +82,14 @@ namespace MapControl MapProjectionPropertyChanged(MapProjection); } - /// - /// Raised when the current map viewport has changed. - /// - public event EventHandler ViewportChanged; + internal Size RenderSize => Bounds.Size; - /// - /// Gets or sets the map foreground Brush. - /// - public IBrush Foreground + protected override void OnSizeChanged(SizeChangedEventArgs e) { - get => GetValue(ForegroundProperty); - set => SetValue(ForegroundProperty, value); - } + base.OnSizeChanged(e); - /// - /// Gets or sets the Duration of the Center, ZoomLevel and Heading animations. - /// The default value is 0.3 seconds. - /// - public TimeSpan AnimationDuration - { - get => GetValue(AnimationDurationProperty); - set => SetValue(AnimationDurationProperty, value); - } - - /// - /// Gets or sets the base map layer, which is added as first element to the Children collection. - /// If the layer implements IMapLayer (like MapTileLayer or MapImageLayer), its (non-null) MapBackground - /// and MapForeground property values are used for the MapBase Background and Foreground properties. - /// - public Control MapLayer - { - get => GetValue(MapLayerProperty); - set => SetValue(MapLayerProperty, value); - } - - /// - /// Gets or sets the MapProjection used by the map control. - /// - public MapProjection MapProjection - { - get => GetValue(MapProjectionProperty); - set => SetValue(MapProjectionProperty, value); - } - - /// - /// Gets or sets an optional center (reference point) for azimuthal projections. - /// If ProjectionCenter is null, the Center property value will be used instead. - /// - public Location ProjectionCenter - { - get => GetValue(ProjectionCenterProperty); - set => SetValue(ProjectionCenterProperty, value); - } - - /// - /// Gets or sets the location of the center point of the map. - /// - public Location Center - { - get => GetValue(CenterProperty); - set => SetValue(CenterProperty, value); - } - - /// - /// Gets or sets the minimum value of the ZoomLevel property. - /// Must not be less than zero or greater than MaxZoomLevel. The default value is 1. - /// - public double MinZoomLevel - { - get => GetValue(MinZoomLevelProperty); - set => SetValue(MinZoomLevelProperty, value); - } - - /// - /// Gets or sets the maximum value of the ZoomLevel property. - /// Must not be less than MinZoomLevel. The default value is 20. - /// - public double MaxZoomLevel - { - get => GetValue(MaxZoomLevelProperty); - set => SetValue(MaxZoomLevelProperty, value); - } - - /// - /// Gets or sets the map zoom level. - /// - public double ZoomLevel - { - get => GetValue(ZoomLevelProperty); - set => SetValue(ZoomLevelProperty, value); - } - - /// - /// Gets or sets the map heading, a counter-clockwise rotation angle in degrees. - /// - public double Heading - { - get => GetValue(HeadingProperty); - set => SetValue(HeadingProperty, value); + ResetTransformCenter(); + UpdateTransform(); } /// @@ -204,19 +101,9 @@ namespace MapControl get => ViewTransform.Scale; } - /// - /// Gets the ViewTransform instance that is used to transform between projected - /// map coordinates and view coordinates. - /// - public ViewTransform ViewTransform { get; } = new ViewTransform(); - - /// - /// Gets the map scale as the horizontal and vertical scaling factors from geographic - /// coordinates to view coordinates at the specified location, as pixels per meter. - /// - public Point GetScale(Location location) + private void SetViewScale(double viewScale) { - return MapProjection.GetRelativeScale(location) * ViewTransform.Scale; + RaisePropertyChanged(ViewScaleProperty, double.NaN, viewScale); } /// @@ -231,278 +118,6 @@ namespace MapControl .Append(Matrix.CreateRotation(ViewTransform.Rotation)); } - /// - /// Transforms a Location in geographic coordinates to a Point in view coordinates. - /// - public Point? LocationToView(Location location) - { - var point = MapProjection.LocationToMap(location); - - return point.HasValue ? ViewTransform.MapToView(point.Value) : null; - } - - /// - /// Transforms a Point in view coordinates to a Location in geographic coordinates. - /// - public Location ViewToLocation(Point point) - { - return MapProjection.MapToLocation(ViewTransform.ViewToMap(point)); - } - - /// - /// Transforms a Rect in view coordinates to a BoundingBox in geographic coordinates. - /// - public BoundingBox ViewRectToBoundingBox(Rect rect) - { - var p1 = ViewTransform.ViewToMap(new Point(rect.X, rect.Y)); - var p2 = ViewTransform.ViewToMap(new Point(rect.X, rect.Y + rect.Height)); - var p3 = ViewTransform.ViewToMap(new Point(rect.X + rect.Width, rect.Y)); - var p4 = ViewTransform.ViewToMap(new Point(rect.X + rect.Width, rect.Y + rect.Height)); - - var x1 = Math.Min(p1.X, Math.Min(p2.X, Math.Min(p3.X, p4.X))); - var y1 = Math.Min(p1.Y, Math.Min(p2.Y, Math.Min(p3.Y, p4.Y))); - var x2 = Math.Max(p1.X, Math.Max(p2.X, Math.Max(p3.X, p4.X))); - var y2 = Math.Max(p1.Y, Math.Max(p2.Y, Math.Max(p3.Y, p4.Y))); - - return MapProjection.MapToBoundingBox(new Rect(x1, y1, x2, y2)); - } - - /// - /// 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. - /// - public void SetTransformCenter(Point center) - { - transformCenter = ViewToLocation(center); - viewCenter = transformCenter != null ? center : new Point(Bounds.Width / 2d, Bounds.Height / 2d); - } - - /// - /// Resets the temporary transform center point set by SetTransformCenter. - /// - public void ResetTransformCenter() - { - transformCenter = null; - viewCenter = new Point(Bounds.Width / 2d, Bounds.Height / 2d); - } - - /// - /// Changes the Center property according to the specified translation in view coordinates. - /// - public void TranslateMap(Point translation) - { - if (transformCenter != null) - { - ResetTransformCenter(); - UpdateTransform(); - } - - if (translation.X != 0d || translation.Y != 0d) - { - var center = ViewToLocation(new Point(viewCenter.X - translation.X, viewCenter.Y - translation.Y)); - - if (center != null) - { - Center = center; - } - } - } - - /// - /// 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. - /// - public void TransformMap(Point center, Point translation, double rotation, double scale) - { - if (rotation != 0d || scale != 1d) - { - SetTransformCenter(center); - - viewCenter += translation; - - if (rotation != 0d) - { - Heading = (((Heading - rotation) % 360d) + 360d) % 360d; - } - - if (scale != 1d) - { - ZoomLevel = Math.Min(Math.Max(ZoomLevel + Math.Log(scale, 2d), MinZoomLevel), MaxZoomLevel); - } - - UpdateTransform(true); - } - else - { - // More accurate than SetTransformCenter. - // - TranslateMap(translation); - } - } - - /// - /// Animates the value of the ZoomLevel property while retaining the specified center point - /// in view coordinates. - /// - public async Task ZoomMap(Point center, double zoomLevel, CancellationToken cancellationToken = default) - { - zoomLevel = Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel); - - if (zoomLevel != ZoomLevel) - { - SetTransformCenter(center); - - var animation = new Animation - { - FillMode = FillMode.Forward, - Duration = AnimationDuration, - Children = - { - new KeyFrame - { - KeyTime = AnimationDuration, - Setters = { new Setter(ZoomLevelProperty, zoomLevel) } - } - } - }; - - await animation.RunAsync(this, cancellationToken); - } - } - - protected override void OnSizeChanged(SizeChangedEventArgs e) - { - base.OnSizeChanged(e); - - ResetTransformCenter(); - UpdateTransform(); - } - - internal double ConstrainedLongitude(double longitude) - { - var offset = longitude - Center.Longitude; - - if (offset > 180d) - { - longitude = Center.Longitude + (offset % 360d) - 360d; - } - else if (offset < -180d) - { - longitude = Center.Longitude + (offset % 360d) + 360d; - } - - return longitude; - } - - private void UpdateTransform(bool resetTransformCenter = false, bool projectionChanged = false) - { - var transformCenterChanged = false; - var oldViewScale = ViewScale; - var viewScale = ViewTransform.ZoomLevelToScale(ZoomLevel); - var projection = MapProjection; - - projection.Center = ProjectionCenter ?? Center; - - var mapCenter = projection.LocationToMap(transformCenter ?? Center); - - if (mapCenter.HasValue) - { - ViewTransform.SetTransform(mapCenter.Value, viewCenter, viewScale, -Heading); - - if (transformCenter != null) - { - var center = ViewToLocation(new Point(Bounds.Width / 2d, Bounds.Height / 2d)); - - if (center != null) - { - var centerLatitude = center.Latitude; - var centerLongitude = Location.NormalizeLongitude(center.Longitude); - - if (centerLatitude < -maxLatitude || centerLatitude > maxLatitude) - { - centerLatitude = Math.Min(Math.Max(centerLatitude, -maxLatitude), maxLatitude); - resetTransformCenter = true; - } - - Center = new Location(centerLatitude, centerLongitude); - - if (resetTransformCenter) - { - // Check if transform center has moved across 180° longitude. - // - transformCenterChanged = Math.Abs(center.Longitude - transformCenter.Longitude) > 180d; - - ResetTransformCenter(); - - projection.Center = ProjectionCenter ?? Center; - - mapCenter = projection.LocationToMap(center); - - if (mapCenter.HasValue) - { - ViewTransform.SetTransform(mapCenter.Value, viewCenter, viewScale, -Heading); - } - } - } - } - - RaisePropertyChanged(ViewScaleProperty, oldViewScale, ViewScale); - - // 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); - } - - private void MapLayerPropertyChanged(AvaloniaPropertyChangedEventArgs args) - { - if (args.OldValue.Value != null) - { - Children.Remove(args.OldValue.Value); - - if (args.OldValue.Value is IMapLayer mapLayer) - { - if (mapLayer.MapBackground != null) - { - ClearValue(BackgroundProperty); - } - if (mapLayer.MapForeground != null) - { - ClearValue(ForegroundProperty); - } - } - } - - if (args.NewValue.Value != null) - { - Children.Insert(0, args.NewValue.Value); - - if (args.NewValue.Value is IMapLayer mapLayer) - { - if (mapLayer.MapBackground != null) - { - Background = mapLayer.MapBackground; - } - if (mapLayer.MapForeground != null) - { - Foreground = mapLayer.MapForeground; - } - } - } - } - private void MapProjectionPropertyChanged(MapProjection projection) { maxLatitude = 90d; @@ -514,6 +129,7 @@ namespace MapControl if (maxLocation != null && maxLocation.Latitude < 90d) { maxLatitude = maxLocation.Latitude; + Center = CoerceCenterProperty(Center); } } @@ -522,12 +138,6 @@ namespace MapControl UpdateTransform(false, true); } - private void ProjectionCenterPropertyChanged() - { - ResetTransformCenter(); - UpdateTransform(); - } - private Location CoerceCenterProperty(Location center) { if (center == null) @@ -546,6 +156,14 @@ namespace MapControl return center; } + private void CenterPropertyChanged(Location center) + { + if (!internalPropertyChange) + { + UpdateTransform(); + } + } + private double CoerceMinZoomLevelProperty(double minZoomLevel) { return Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel); @@ -560,5 +178,109 @@ namespace MapControl { return Math.Min(Math.Max(zoomLevel, MinZoomLevel), 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 = new Animation + { + FillMode = FillMode.Forward, + Duration = AnimationDuration, + Children = + { + new KeyFrame + { + KeyTime = AnimationDuration, + Setters = { new Setter(ZoomLevelProperty, targetZoomLevel) } + } + } + }; + + zoomLevelCts = new CancellationTokenSource(); + + await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token); + + zoomLevelCts.Dispose(); + zoomLevelCts = null; + zoomLevelAnimation = null; + } + } + + private static double CoerceHeadingProperty(double heading) + { + return ((heading % 360d) + 360d) % 360d; + } + + 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 = new Animation + { + FillMode = FillMode.Forward, + Duration = AnimationDuration, + Children = + { + new KeyFrame + { + KeyTime = AnimationDuration, + Setters = { new Setter(HeadingProperty, targetHeading) } + } + } + }; + + headingCts = new CancellationTokenSource(); + + await headingAnimation.RunAsync(this, headingCts.Token); + + headingCts.Dispose(); + headingCts = null; + headingAnimation = null; + } + } } } diff --git a/MapControl/Avalonia/MapControl.Avalonia.csproj b/MapControl/Avalonia/MapControl.Avalonia.csproj index e0d0e853..8b65de71 100644 --- a/MapControl/Avalonia/MapControl.Avalonia.csproj +++ b/MapControl/Avalonia/MapControl.Avalonia.csproj @@ -21,7 +21,11 @@ + + + + @@ -30,6 +34,7 @@ + diff --git a/MapControl/Avalonia/MapPanel.Avalonia.cs b/MapControl/Avalonia/MapPanel.Avalonia.cs index ed6d0f54..1bfb51fe 100644 --- a/MapControl/Avalonia/MapPanel.Avalonia.cs +++ b/MapControl/Avalonia/MapPanel.Avalonia.cs @@ -4,341 +4,35 @@ using Avalonia.Controls; using Avalonia.Media; -using System; namespace MapControl { - public class MapPanel : Panel + public partial class MapPanel { - public static readonly AttachedProperty ParentMapProperty - = AvaloniaProperty.RegisterAttached("ParentMap", null, true); - - private static readonly AttachedProperty ViewPositionProperty - = AvaloniaProperty.RegisterAttached("ViewPosition"); - - public static readonly AttachedProperty LocationProperty - = AvaloniaProperty.RegisterAttached("Location"); - - public static readonly AttachedProperty BoundingBoxProperty - = AvaloniaProperty.RegisterAttached("BoundingBox"); - - public static readonly AttachedProperty AutoCollapseProperty - = AvaloniaProperty.RegisterAttached("AutoCollapse"); - - static MapPanel() - { - ParentMapProperty.Changed.AddClassHandler( - (panel, args) => panel.OnParentMapPropertyChanged(args.NewValue.Value)); - } - public MapPanel() { - ClipToBounds = true; - if (this is MapBase mapBase) { - SetParentMap(this, mapBase); + SetValue(ParentMapProperty, mapBase); } } - public MapBase ParentMap { get; private set; } - - public static MapBase GetParentMap(AvaloniaObject obj) => obj.GetValue(ParentMapProperty); - - private static void SetParentMap(AvaloniaObject obj, MapBase value) => obj.SetValue(ParentMapProperty, value); - - public static Point? GetViewPosition(AvaloniaObject obj) => obj.GetValue(ViewPositionProperty); - - private static void SetViewPosition(AvaloniaObject obj, Point? value) => obj.SetValue(ViewPositionProperty, value); - - public static Location GetLocation(AvaloniaObject obj) => obj.GetValue(LocationProperty); - - public static void SetLocation(AvaloniaObject obj, Location value) => obj.SetValue(LocationProperty, value); - - public static BoundingBox GetBoundingBox(AvaloniaObject obj) => obj.GetValue(BoundingBoxProperty); - - public static void SetBoundingBox(AvaloniaObject obj, BoundingBox value) => obj.SetValue(BoundingBoxProperty, value); - - public static bool GetAutoCollapse(AvaloniaObject obj) => obj.GetValue(AutoCollapseProperty); - - public static void SetAutoCollapse(AvaloniaObject obj, bool value) => obj.SetValue(AutoCollapseProperty, value); - - protected virtual void OnViewportChanged(ViewportChangedEventArgs e) + public static MapBase GetParentMap(Control element) { - InvalidateArrange(); + return (MapBase)element.GetValue(ParentMapProperty); } - protected override Size MeasureOverride(Size availableSize) + public static void SetRenderTransform(Control element, Transform transform, double originX = 0d, double originY = 0d) { - availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity); - - foreach (var element in Children) - { - element.Measure(availableSize); - } - - return new Size(); + element.RenderTransform = transform; + element.RenderTransformOrigin = new RelativePoint(originX, originY, RelativeUnit.Relative); } - protected override Size ArrangeOverride(Size finalSize) + private Controls ChildElements => Children; + + private static void SetVisible(Control element, bool visible) { - if (ParentMap != null) - { - foreach (var element in Children) - { - var location = GetLocation(element); - var position = location != null ? GetViewPosition(location) : null; - - SetViewPosition(element, position); - - if (GetAutoCollapse(element)) - { - element.IsVisible = !(position.HasValue && IsOutsideViewport(position.Value)); - } - - if (position.HasValue) - { - ArrangeElement(element, position.Value); - } - else - { - var boundingBox = GetBoundingBox(element); - - if (boundingBox != null) - { - var viewRect = GetViewRect(boundingBox); - - if (viewRect.HasValue) - { - ArrangeElement(element, viewRect.Value); - } - } - 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 rect = ParentMap.MapProjection.BoundingBoxToMap(boundingBox); - - if (!rect.HasValue) - { - return null; - } - - return GetViewRect(rect.Value); - } - - protected ViewRect GetViewRect(Rect 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) - { - var pos = ParentMap.LocationToView( - new Location(location.Latitude, ParentMap.ConstrainedLongitude(location.Longitude))); - - 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.Bounds.Width - || point.Y < 0d || point.Y > ParentMap.Bounds.Height; - } - - private static void ArrangeElement(Control element, Point position) - { - var size = GetDesiredSize(element); - var x = position.X; - var y = position.Y; - - switch (element.HorizontalAlignment) - { - case Avalonia.Layout.HorizontalAlignment.Center: - x -= size.Width / 2d; - break; - - case Avalonia.Layout.HorizontalAlignment.Right: - x -= size.Width; - break; - - default: - break; - } - - switch (element.VerticalAlignment) - { - case Avalonia.Layout.VerticalAlignment.Center: - y -= size.Height / 2d; - break; - - case Avalonia.Layout.VerticalAlignment.Bottom: - y -= size.Height; - break; - - default: - break; - } - - ArrangeElement(element, new Rect(x, y, size.Width, size.Height)); - } - - private static void ArrangeElement(Control element, Size parentSize) - { - var size = GetDesiredSize(element); - var x = 0d; - var y = 0d; - var width = size.Width; - var height = size.Height; - - switch (element.HorizontalAlignment) - { - case Avalonia.Layout.HorizontalAlignment.Center: - x = (parentSize.Width - size.Width) / 2d; - break; - - case Avalonia.Layout.HorizontalAlignment.Right: - x = parentSize.Width - size.Width; - break; - - case Avalonia.Layout.HorizontalAlignment.Stretch: - width = parentSize.Width; - break; - - default: - break; - } - - switch (element.VerticalAlignment) - { - case Avalonia.Layout.VerticalAlignment.Center: - y = (parentSize.Height - size.Height) / 2d; - break; - - case Avalonia.Layout.VerticalAlignment.Bottom: - y = parentSize.Height - size.Height; - break; - - case Avalonia.Layout.VerticalAlignment.Stretch: - height = parentSize.Height; - break; - - default: - break; - } - - ArrangeElement(element, new Rect(x, y, width, height)); - } - - private static void ArrangeElement(Control element, ViewRect rect) - { - element.Width = rect.Rect.Width; - element.Height = rect.Rect.Height; - - ArrangeElement(element, rect.Rect); - - 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 RelativePoint(0.5, 0.5, RelativeUnit.Relative); - } - } - - private static void ArrangeElement(Control element, Rect rect) - { - if (element.UseLayoutRounding) - { - rect = new Rect(Math.Round(rect.X), Math.Round(rect.Y), Math.Round(rect.Width), Math.Round(rect.Height)); - } - - element.Arrange(rect); - } - - internal static Size GetDesiredSize(Control 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); - } - - private void OnViewportChanged(object sender, ViewportChangedEventArgs e) - { - OnViewportChanged(e); - } - - private void OnParentMapPropertyChanged(MapBase parentMap) - { - if (ParentMap != null && ParentMap != this) - { - ParentMap.ViewportChanged -= OnViewportChanged; - } - - ParentMap = parentMap; - - if (ParentMap != null && ParentMap != this) - { - ParentMap.ViewportChanged += OnViewportChanged; - - OnViewportChanged(new ViewportChangedEventArgs()); - } + element.IsVisible = visible; } } } diff --git a/MapControl/Avalonia/MapTileLayer.Avalonia.cs b/MapControl/Avalonia/MapTileLayer.Avalonia.cs deleted file mode 100644 index dbd9f28e..00000000 --- a/MapControl/Avalonia/MapTileLayer.Avalonia.cs +++ /dev/null @@ -1,231 +0,0 @@ -// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control -// Copyright © 2024 Clemens Fischer -// Licensed under the Microsoft Public License (Ms-PL) - -using Avalonia.Media; -using System; -using System.Threading.Tasks; - -namespace MapControl -{ - /// - /// Displays a standard Web Mercator map tile grid, e.g. an OpenStreetMap tile grid. - /// - public class MapTileLayer : MapTileLayerBase - { - private const int TileSize = 256; - - private static readonly Point MapTopLeft = new( - -180d * MapProjection.Wgs84MeterPerDegree, 180d * MapProjection.Wgs84MeterPerDegree); - - /// - /// A default MapTileLayer using OpenStreetMap data. - /// - public static MapTileLayer OpenStreetMapTileLayer => new() - { - TileSource = new TileSource { UriTemplate = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" }, - SourceName = "OpenStreetMap", - Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)" - }; - - public static readonly StyledProperty MinZoomLevelProperty - = AvaloniaProperty.Register(nameof(MinZoomLevel)); - - public static readonly StyledProperty MaxZoomLevelProperty - = AvaloniaProperty.Register(nameof(MaxZoomLevel), 19); - - public static readonly StyledProperty ZoomLevelOffsetProperty - = AvaloniaProperty.Register(nameof(ZoomLevelOffset)); - - public TileMatrix TileMatrix { get; private set; } - - public TileCollection Tiles { get; private set; } = []; - - /// - /// Minimum zoom level supported by the MapTileLayer. Default value is 0. - /// - public int MinZoomLevel - { - get => GetValue(MinZoomLevelProperty); - set => SetValue(MinZoomLevelProperty, value); - } - - /// - /// Maximum zoom level supported by the MapTileLayer. Default value is 19. - /// - public int MaxZoomLevel - { - get => GetValue(MaxZoomLevelProperty); - set => SetValue(MaxZoomLevelProperty, value); - } - - /// - /// Optional offset between the map zoom level and the topmost tile zoom level. - /// Default value is 0. - /// - public double ZoomLevelOffset - { - get => GetValue(ZoomLevelOffsetProperty); - set => SetValue(ZoomLevelOffsetProperty, value); - } - - protected override Size MeasureOverride(Size availableSize) - { - availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity); - - foreach (var tile in Tiles) - { - tile.Image.Measure(availableSize); - } - - return new Size(); - } - - protected override Size ArrangeOverride(Size finalSize) - { - if (TileMatrix != null) - { - foreach (var tile in Tiles) - { - // Arrange tiles relative to 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 Task UpdateTileLayer(bool tileSourceChanged) - { - var updateTiles = false; - - if (ParentMap == null || ParentMap.MapProjection.Type != MapProjectionType.WebMercator) - { - updateTiles = TileMatrix != null; - TileMatrix = null; - } - else - { - if (tileSourceChanged) - { - Tiles = []; // clear all - updateTiles = true; - } - - if (SetTileMatrix()) - { - updateTiles = true; - } - - SetRenderTransform(); - } - - if (updateTiles) - { - UpdateTiles(); - - return LoadTiles(Tiles, SourceName); - } - - return Task.CompletedTask; - } - - protected override void SetRenderTransform() - { - if (TileMatrix != null) - { - // Tile matrix origin in pixels. - // - var tileMatrixOrigin = new Point(TileSize * TileMatrix.XMin, TileSize * TileMatrix.YMin); - - var tileMatrixScale = ViewTransform.ZoomLevelToScale(TileMatrix.ZoomLevel); - - ((MatrixTransform)RenderTransform).Matrix = - ParentMap.ViewTransform.GetTileLayerTransform(tileMatrixScale, MapTopLeft, tileMatrixOrigin); - } - } - - private bool SetTileMatrix() - { - // Add 0.001 to avoid rounding issues. - // - var tileMatrixZoomLevel = (int)Math.Floor(ParentMap.ZoomLevel - ZoomLevelOffset + 0.001); - - var tileMatrixScale = ViewTransform.ZoomLevelToScale(tileMatrixZoomLevel); - - // Bounds in tile pixels from view size. - // - var bounds = ParentMap.ViewTransform.GetTileMatrixBounds(tileMatrixScale, MapTopLeft, ParentMap.Bounds.Size); - - // 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 TileCollection(); - - if (TileSource != null && TileMatrix != null) - { - var maxZoomLevel = Math.Min(TileMatrix.ZoomLevel, MaxZoomLevel); - - if (maxZoomLevel >= MinZoomLevel) - { - var minZoomLevel = IsBaseMapLayer - ? Math.Max(TileMatrix.ZoomLevel - MaxBackgroundLevels, MinZoomLevel) - : maxZoomLevel; - - for (var z = minZoomLevel; z <= maxZoomLevel; z++) - { - var numTiles = 1 << z; - var tileSize = 1 << (TileMatrix.ZoomLevel - z); - var x1 = (int)Math.Floor((double)TileMatrix.XMin / tileSize); // may be negative - var x2 = TileMatrix.XMax / tileSize; // may be greater than numTiles-1 - var y1 = Math.Max(TileMatrix.YMin / tileSize, 0); - var y2 = Math.Min(TileMatrix.YMax / tileSize, numTiles - 1); - - for (var y = y1; y <= y2; y++) - { - for (var x = x1; x <= x2; x++) - { - tiles.Add(Tiles.GetTile(z, x, y, numTiles)); - } - } - } - } - } - - Tiles = tiles; - - Children.Clear(); - - foreach (var tile in tiles) - { - Children.Add(tile.Image); - } - } - } -} diff --git a/MapControl/Avalonia/MapTileLayerBase.Avalonia.cs b/MapControl/Avalonia/MapTileLayerBase.Avalonia.cs deleted file mode 100644 index a10b6ce5..00000000 --- a/MapControl/Avalonia/MapTileLayerBase.Avalonia.cs +++ /dev/null @@ -1,214 +0,0 @@ -// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control -// Copyright © 2024 Clemens Fischer -// Licensed under the Microsoft Public License (Ms-PL) - -using Avalonia.Controls; -using Avalonia.Media; -using Avalonia.Threading; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace MapControl -{ - public abstract class MapTileLayerBase : Panel - { - public static readonly StyledProperty TileSourceProperty - = AvaloniaProperty.Register(nameof(TileSource)); - - public static readonly StyledProperty SourceNameProperty - = AvaloniaProperty.Register(nameof(SourceName)); - - public static readonly StyledProperty DescriptionProperty - = AvaloniaProperty.Register(nameof(Description)); - - public static readonly StyledProperty MaxBackgroundLevelsProperty - = AvaloniaProperty.Register(nameof(MaxBackgroundLevels), 5); - - public static readonly StyledProperty UpdateIntervalProperty - = AvaloniaProperty.Register(nameof(AvaloniaProperty), TimeSpan.FromSeconds(0.2)); - - public static readonly StyledProperty UpdateWhileViewportChangingProperty - = AvaloniaProperty.Register(nameof(UpdateWhileViewportChanging)); - - public static readonly StyledProperty MapBackgroundProperty - = AvaloniaProperty.Register(nameof(MapBackground)); - - public static readonly StyledProperty MapForegroundProperty - = AvaloniaProperty.Register(nameof(MapForeground)); - - public static readonly DirectProperty LoadingProgressProperty - = AvaloniaProperty.RegisterDirect(nameof(LoadingProgress), layer => layer.loadingProgressValue); - - private readonly DispatcherTimer updateTimer; - private readonly Progress loadingProgress; - private double loadingProgressValue; - private ITileImageLoader tileImageLoader; - - static MapTileLayerBase() - { - MapPanel.ParentMapProperty.Changed.AddClassHandler( - (layer, args) => layer.OnParentMapPropertyChanged(args.NewValue.Value)); - - TileSourceProperty.Changed.AddClassHandler( - async (layer, args) => await layer.Update(true)); - - UpdateIntervalProperty.Changed.AddClassHandler( - (layer, args) => layer.updateTimer.Interval = args.NewValue.Value); - } - - protected MapTileLayerBase() - { - RenderTransform = new MatrixTransform(); - RenderTransformOrigin = new RelativePoint(); - - loadingProgress = new Progress(p => SetAndRaise(LoadingProgressProperty, ref loadingProgressValue, p)); - - updateTimer = this.CreateTimer(UpdateInterval); - updateTimer.Tick += async (s, e) => await Update(false); - } - - public MapBase ParentMap { get; private set; } - - public ITileImageLoader TileImageLoader - { - get => tileImageLoader ??= new TileImageLoader(); - set => tileImageLoader = value; - } - - /// - /// Provides map tile URIs or images. - /// - public TileSource TileSource - { - get => GetValue(TileSourceProperty); - set => SetValue(TileSourceProperty, value); - } - - /// - /// Name of the TileSource. Used as component of a tile cache key. - /// - public string SourceName - { - get => GetValue(SourceNameProperty); - set => SetValue(SourceNameProperty, value); - } - - /// - /// Description of the layer. Used to display copyright information on top of the map. - /// - public string Description - { - get => GetValue(DescriptionProperty); - set => SetValue(DescriptionProperty, value); - } - - /// - /// Maximum number of background tile levels. Default value is 5. - /// Only effective in a MapTileLayer or WmtsTileLayer that is the MapLayer of its ParentMap. - /// - public int MaxBackgroundLevels - { - get => GetValue(MaxBackgroundLevelsProperty); - set => SetValue(MaxBackgroundLevelsProperty, value); - } - - /// - /// Minimum time interval between tile updates. - /// - public TimeSpan UpdateInterval - { - get => GetValue(UpdateIntervalProperty); - set => SetValue(UpdateIntervalProperty, value); - } - - /// - /// Controls if tiles are updated while the viewport is still changing. - /// - public bool UpdateWhileViewportChanging - { - get => GetValue(UpdateWhileViewportChangingProperty); - set => SetValue(UpdateWhileViewportChangingProperty, value); - } - - /// - /// Optional background brush. Sets MapBase.Background if not null and this layer is the base map layer. - /// - public IBrush MapBackground - { - get => GetValue(MapBackgroundProperty); - set => SetValue(MapBackgroundProperty, value); - } - - /// - /// Optional foreground brush. Sets MapBase.Foreground if not null and this layer is the base map layer. - /// - public IBrush MapForeground - { - get => GetValue(MapForegroundProperty); - set => SetValue(MapForegroundProperty, value); - } - - /// - /// Gets the progress of the TileImageLoader as a double value between 0 and 1. - /// - public double LoadingProgress => loadingProgressValue; - - protected bool IsBaseMapLayer - { - get - { - var parentMap = MapPanel.GetParentMap(this); - - return parentMap != null && parentMap.Children.Count > 0 && parentMap.Children[0] == this; - } - } - - protected abstract void SetRenderTransform(); - - protected abstract Task UpdateTileLayer(bool tileSourceChanged); - - protected Task LoadTiles(IEnumerable tiles, string cacheName) - { - return TileImageLoader.LoadTilesAsync(tiles, TileSource, cacheName, loadingProgress); - } - - private Task Update(bool tileSourceChanged) - { - updateTimer.Stop(); - - return UpdateTileLayer(tileSourceChanged); - } - - private async void OnViewportChanged(object sender, ViewportChangedEventArgs e) - { - if (e.TransformCenterChanged || e.ProjectionChanged || Children.Count == 0) - { - await Update(false); // update immediately - } - else - { - SetRenderTransform(); - - updateTimer.Run(!UpdateWhileViewportChanging); - } - } - - private void OnParentMapPropertyChanged(MapBase parentMap) - { - if (ParentMap != null) - { - ParentMap.ViewportChanged -= OnViewportChanged; - } - - ParentMap = parentMap; - - if (ParentMap != null) - { - ParentMap.ViewportChanged += OnViewportChanged; - } - - updateTimer.Run(); - } - } -} diff --git a/MapControl/Avalonia/Timer.Avalonia.cs b/MapControl/Avalonia/Timer.Avalonia.cs deleted file mode 100644 index dca713c8..00000000 --- a/MapControl/Avalonia/Timer.Avalonia.cs +++ /dev/null @@ -1,35 +0,0 @@ -// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control -// Copyright © 2024 Clemens Fischer -// Licensed under the Microsoft Public License (Ms-PL) - -using Avalonia.Threading; -using System; - -namespace MapControl -{ - internal static class Timer - { - public static DispatcherTimer CreateTimer(this AvaloniaObject obj, TimeSpan interval) - { - var timer = new DispatcherTimer - { - Interval = interval - }; - - return timer; - } - - public static void Run(this DispatcherTimer timer, bool restart = false) - { - if (restart) - { - timer.Stop(); - } - - if (!timer.IsEnabled) - { - timer.Start(); - } - } - } -} diff --git a/MapControl/Shared/DependencyPropertyHelper.cs b/MapControl/Shared/DependencyPropertyHelper.cs new file mode 100644 index 00000000..505203dc --- /dev/null +++ b/MapControl/Shared/DependencyPropertyHelper.cs @@ -0,0 +1,71 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// Copyright © 2024 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using System; +#if WINUI +using Microsoft.UI.Xaml; +#elif UWP +using Windows.UI.Xaml; +#else +using System.Windows; +#endif + +namespace MapControl +{ + public static class DependencyPropertyHelper + { + public static DependencyProperty Register( + string name, + TValue defaultValue = default, + bool bindTwoWayByDefault = false, + Action propertyChanged = null) + where TOwner : DependencyObject + { +#if WINUI || UWP + var metadata = propertyChanged != null + ? new PropertyMetadata(defaultValue, (o, e) => propertyChanged((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue)) + : new PropertyMetadata(defaultValue); + +#else + var metadata = new FrameworkPropertyMetadata + { + DefaultValue = defaultValue, + BindsTwoWayByDefault = bindTwoWayByDefault + }; + + if (propertyChanged != null) + { + metadata.PropertyChangedCallback = (o, e) => propertyChanged((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue); + } +#endif + return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata); + } + + public static DependencyProperty RegisterAttached( + string name, + TValue defaultValue = default, + bool inherits = false, + Action propertyChanged = null) + where TOwner : DependencyObject + { +#if WINUI || UWP + var metadata = propertyChanged != null + ? new PropertyMetadata(defaultValue, (o, e) => propertyChanged((FrameworkElement)o, (TValue)e.OldValue, (TValue)e.NewValue)) + : new PropertyMetadata(defaultValue); +#else + var metadata = new FrameworkPropertyMetadata + { + DefaultValue = defaultValue, + Inherits = inherits + }; + + if (propertyChanged != null) + { + metadata.PropertyChangedCallback = (o, e) => propertyChanged((FrameworkElement)o, (TValue)e.OldValue, (TValue)e.NewValue); + } +#endif + return DependencyProperty.RegisterAttached(name, typeof(TValue), typeof(TOwner), metadata); + } + } +} diff --git a/MapControl/Shared/FilePath.cs b/MapControl/Shared/FilePath.cs index 98325452..1cc602dc 100644 --- a/MapControl/Shared/FilePath.cs +++ b/MapControl/Shared/FilePath.cs @@ -2,7 +2,6 @@ // Copyright © 2024 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) -using System; using System.IO; namespace MapControl @@ -12,7 +11,7 @@ namespace MapControl public static string GetFullPath(string path) { #if NET6_0_OR_GREATER - return Path.GetFullPath(path, AppDomain.CurrentDomain.BaseDirectory); + return Path.GetFullPath(path, System.AppDomain.CurrentDomain.BaseDirectory); #else return Path.GetFullPath(path); #endif diff --git a/MapControl/Shared/MapBase.cs b/MapControl/Shared/MapBase.cs index 41f2a788..95f3bb17 100644 --- a/MapControl/Shared/MapBase.cs +++ b/MapControl/Shared/MapBase.cs @@ -4,14 +4,10 @@ using System; #if WINUI -using Windows.Foundation; using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Animation; #elif UWP -using Windows.Foundation; using Windows.UI.Xaml; -using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Animation; #else using System.Windows; @@ -21,35 +17,8 @@ using System.Windows.Media.Animation; namespace MapControl { - public interface IMapLayer : IMapElement + public partial class MapBase { - Brush MapBackground { get; } - Brush MapForeground { get; } - } - - /// - /// The map control. Displays map content provided by one or more tile or image layers, - /// such as MapTileLayerBase or MapImageLayer instances. - /// The visible map area is defined by the Center and ZoomLevel properties. - /// The map can be rotated by an angle that is given by the Heading property. - /// MapBase can contain map overlay child elements like other MapPanels or MapItemsControls. - /// - public partial class MapBase : MapPanel - { - public static TimeSpan ImageFadeDuration { get; set; } = TimeSpan.FromSeconds(0.1); - - public static readonly DependencyProperty MapLayerProperty = DependencyProperty.Register( - nameof(MapLayer), typeof(UIElement), typeof(MapBase), - new PropertyMetadata(null, (o, e) => ((MapBase)o).MapLayerPropertyChanged((UIElement)e.OldValue, (UIElement)e.NewValue))); - - public static readonly DependencyProperty MapProjectionProperty = DependencyProperty.Register( - nameof(MapProjection), typeof(MapProjection), typeof(MapBase), - new PropertyMetadata(new WebMercatorProjection(), (o, e) => ((MapBase)o).MapProjectionPropertyChanged((MapProjection)e.NewValue))); - - public static readonly DependencyProperty ProjectionCenterProperty = DependencyProperty.Register( - nameof(ProjectionCenter), typeof(Location), typeof(MapBase), - new PropertyMetadata(null, (o, e) => ((MapBase)o).ProjectionCenterPropertyChanged())); - public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register( nameof(MinZoomLevel), typeof(double), typeof(MapBase), new PropertyMetadata(1d, (o, e) => ((MapBase)o).MinZoomLevelPropertyChanged((double)e.NewValue))); @@ -58,10 +27,6 @@ namespace MapControl nameof(MaxZoomLevel), typeof(double), typeof(MapBase), new PropertyMetadata(20d, (o, e) => ((MapBase)o).MaxZoomLevelPropertyChanged((double)e.NewValue))); - public static readonly DependencyProperty AnimationDurationProperty = DependencyProperty.Register( - nameof(AnimationDuration), typeof(TimeSpan), typeof(MapBase), - new PropertyMetadata(TimeSpan.FromSeconds(0.3))); - public static readonly DependencyProperty AnimationEasingFunctionProperty = DependencyProperty.Register( nameof(AnimationEasingFunction), typeof(EasingFunctionBase), typeof(MapBase), new PropertyMetadata(new QuadraticEase { EasingMode = EasingMode.EaseOut })); @@ -69,139 +34,6 @@ namespace MapControl private PointAnimation centerAnimation; private DoubleAnimation zoomLevelAnimation; private DoubleAnimation headingAnimation; - private Location transformCenter; - private Point viewCenter; - private double centerLongitude; - private double maxLatitude = 90d; - private bool internalPropertyChange; - - /// - /// Raised when the current map viewport has changed. - /// - public event EventHandler ViewportChanged; - - /// - /// Gets or sets the map foreground Brush. - /// - public Brush Foreground - { - get => (Brush)GetValue(ForegroundProperty); - set => SetValue(ForegroundProperty, value); - } - - /// - /// Gets or sets the base map layer, which is added as first element to the Children collection. - /// If the layer implements IMapLayer (like MapTileLayer or MapImageLayer), its (non-null) MapBackground - /// and MapForeground property values are used for the MapBase Background and Foreground properties. - /// - public UIElement MapLayer - { - get => (UIElement)GetValue(MapLayerProperty); - set => SetValue(MapLayerProperty, value); - } - - /// - /// Gets or sets the MapProjection used by the map control. - /// - public MapProjection MapProjection - { - get => (MapProjection)GetValue(MapProjectionProperty); - set => SetValue(MapProjectionProperty, value); - } - - /// - /// Gets or sets an optional center (reference point) for azimuthal projections. - /// If ProjectionCenter is null, the Center property value will be used instead. - /// - public Location ProjectionCenter - { - get => (Location)GetValue(ProjectionCenterProperty); - set => SetValue(ProjectionCenterProperty, value); - } - - /// - /// Gets or sets the location of the center point of the map. - /// - public Location Center - { - get => (Location)GetValue(CenterProperty); - set => SetValue(CenterProperty, value); - } - - /// - /// Gets or sets the target value of a Center animation. - /// - public Location TargetCenter - { - get => (Location)GetValue(TargetCenterProperty); - set => SetValue(TargetCenterProperty, value); - } - - /// - /// 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. - /// - public double MinZoomLevel - { - get => (double)GetValue(MinZoomLevelProperty); - set => SetValue(MinZoomLevelProperty, value); - } - - /// - /// Gets or sets the maximum value of the ZoomLevel and TargetZoomLevel properties. - /// Must not be less than MinZoomLevel. The default value is 20. - /// - public double MaxZoomLevel - { - get => (double)GetValue(MaxZoomLevelProperty); - set => SetValue(MaxZoomLevelProperty, value); - } - - /// - /// Gets or sets the map zoom level. - /// - public double ZoomLevel - { - get => (double)GetValue(ZoomLevelProperty); - set => SetValue(ZoomLevelProperty, value); - } - - /// - /// Gets or sets the target value of a ZoomLevel animation. - /// - public double TargetZoomLevel - { - get => (double)GetValue(TargetZoomLevelProperty); - set => SetValue(TargetZoomLevelProperty, value); - } - - /// - /// Gets or sets the map heading, a counter-clockwise rotation angle in degrees. - /// - public double Heading - { - get => (double)GetValue(HeadingProperty); - set => SetValue(HeadingProperty, value); - } - - /// - /// Gets or sets the target value of a Heading animation. - /// - public double TargetHeading - { - get => (double)GetValue(TargetHeadingProperty); - set => SetValue(TargetHeadingProperty, value); - } - - /// - /// Gets or sets the Duration of the Center, ZoomLevel and Heading animations. - /// The default value is 0.3 seconds. - /// - public TimeSpan AnimationDuration - { - get => (TimeSpan)GetValue(AnimationDurationProperty); - set => SetValue(AnimationDurationProperty, value); - } /// /// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations. @@ -219,23 +51,6 @@ namespace MapControl /// public double ViewScale => (double)GetValue(ViewScaleProperty); - /// - /// Gets the ViewTransform instance that is used to transform between projected - /// map coordinates and view coordinates. - /// - public ViewTransform ViewTransform { get; } = new ViewTransform(); - - /// - /// Gets the map scale as the horizontal and vertical scaling factors from geographic - /// coordinates to view coordinates at the specified location, as pixels per meter. - /// - public Point GetScale(Location location) - { - var relativeScale = MapProjection.GetRelativeScale(location); - - return new Point(ViewTransform.Scale * relativeScale.X, ViewTransform.Scale * relativeScale.Y); - } - /// /// Gets a transform Matrix for scaling and rotating objects that are anchored /// at a Location from map coordinates (i.e. meters) to view coordinates. @@ -250,220 +65,6 @@ namespace MapControl return transform; } - /// - /// Transforms a Location in geographic coordinates to a Point in view coordinates. - /// - public Point? LocationToView(Location location) - { - var point = MapProjection.LocationToMap(location); - - if (!point.HasValue) - { - return null; - } - - return ViewTransform.MapToView(point.Value); - } - - /// - /// Transforms a Point in view coordinates to a Location in geographic coordinates. - /// - public Location ViewToLocation(Point point) - { - return MapProjection.MapToLocation(ViewTransform.ViewToMap(point)); - } - - /// - /// Transforms a Rect in view coordinates to a BoundingBox in geographic coordinates. - /// - public BoundingBox ViewRectToBoundingBox(Rect rect) - { - var p1 = ViewTransform.ViewToMap(new Point(rect.X, rect.Y)); - var p2 = ViewTransform.ViewToMap(new Point(rect.X, rect.Y + rect.Height)); - var p3 = ViewTransform.ViewToMap(new Point(rect.X + rect.Width, rect.Y)); - var p4 = ViewTransform.ViewToMap(new Point(rect.X + rect.Width, rect.Y + rect.Height)); - - var x1 = Math.Min(p1.X, Math.Min(p2.X, Math.Min(p3.X, p4.X))); - var y1 = Math.Min(p1.Y, Math.Min(p2.Y, Math.Min(p3.Y, p4.Y))); - var x2 = Math.Max(p1.X, Math.Max(p2.X, Math.Max(p3.X, p4.X))); - var y2 = Math.Max(p1.Y, Math.Max(p2.Y, Math.Max(p3.Y, p4.Y))); - - return MapProjection.MapToBoundingBox(new Rect(x1, y1, x2 - x1, y2 - y1)); - } - - /// - /// 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. - /// - public void SetTransformCenter(Point center) - { - transformCenter = ViewToLocation(center); - viewCenter = transformCenter != null ? center : new Point(RenderSize.Width / 2d, RenderSize.Height / 2d); - } - - /// - /// Resets the temporary transform center point set by SetTransformCenter. - /// - public void ResetTransformCenter() - { - transformCenter = null; - viewCenter = new Point(RenderSize.Width / 2d, RenderSize.Height / 2d); - } - - /// - /// Changes the Center property according to the specified translation in view coordinates. - /// - public void TranslateMap(Point translation) - { - if (transformCenter != null) - { - ResetTransformCenter(); - UpdateTransform(); - } - - if (translation.X != 0d || translation.Y != 0d) - { - var center = ViewToLocation(new Point(viewCenter.X - translation.X, viewCenter.Y - translation.Y)); - - if (center != null) - { - Center = center; - } - } - } - - /// - /// 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. - /// - public void TransformMap(Point center, Point translation, double rotation, double scale) - { - if (rotation != 0d || scale != 1d) - { - SetTransformCenter(center); - - viewCenter = new Point(viewCenter.X + translation.X, viewCenter.Y + translation.Y); - - if (rotation != 0d) - { - var heading = (((Heading - rotation) % 360d) + 360d) % 360d; - - SetValueInternal(HeadingProperty, heading); - SetValueInternal(TargetHeadingProperty, heading); - } - - if (scale != 1d) - { - var zoomLevel = Math.Min(Math.Max(ZoomLevel + Math.Log(scale, 2d), MinZoomLevel), MaxZoomLevel); - - SetValueInternal(ZoomLevelProperty, zoomLevel); - SetValueInternal(TargetZoomLevelProperty, zoomLevel); - } - - UpdateTransform(true); - } - else - { - // More accurate than SetTransformCenter. - // - TranslateMap(translation); - } - } - - /// - /// Sets the value of the TargetZoomLevel property while retaining the specified center point - /// in view coordinates. - /// - public void ZoomMap(Point center, double zoomLevel) - { - zoomLevel = Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel); - - if (TargetZoomLevel != zoomLevel) - { - SetTransformCenter(center); - TargetZoomLevel = zoomLevel; - } - } - - /// - /// Sets the TargetZoomLevel and TargetCenter properties so that the specified bounding box - /// fits into the current view. The TargetHeading property is set to zero. - /// - public void ZoomToBounds(BoundingBox boundingBox) - { - var rect = MapProjection.BoundingBoxToMap(boundingBox); - - if (rect.HasValue) - { - var rectCenter = new Point(rect.Value.X + rect.Value.Width / 2d, rect.Value.Y + rect.Value.Height / 2d); - var targetCenter = MapProjection.MapToLocation(rectCenter); - - if (targetCenter != null) - { - var scale = Math.Min(RenderSize.Width / rect.Value.Width, RenderSize.Height / rect.Value.Height); - - TargetZoomLevel = ViewTransform.ScaleToZoomLevel(scale); - TargetCenter = targetCenter; - TargetHeading = 0d; - } - } - } - - internal double ConstrainedLongitude(double longitude) - { - var offset = longitude - Center.Longitude; - - if (offset > 180d) - { - longitude = Center.Longitude + (offset % 360d) - 360d; - } - else if (offset < -180d) - { - longitude = Center.Longitude + (offset % 360d) + 360d; - } - - return longitude; - } - - private void MapLayerPropertyChanged(UIElement oldLayer, UIElement newLayer) - { - if (oldLayer != null) - { - Children.Remove(oldLayer); - - if (oldLayer is IMapLayer mapLayer) - { - if (mapLayer.MapBackground != null) - { - ClearValue(BackgroundProperty); - } - if (mapLayer.MapForeground != null) - { - ClearValue(ForegroundProperty); - } - } - } - - if (newLayer != null) - { - Children.Insert(0, newLayer); - - if (newLayer is IMapLayer mapLayer) - { - if (mapLayer.MapBackground != null) - { - Background = mapLayer.MapBackground; - } - if (mapLayer.MapForeground != null) - { - Foreground = mapLayer.MapForeground; - } - } - } - } - private void MapProjectionPropertyChanged(MapProjection projection) { maxLatitude = 90d; @@ -484,12 +85,6 @@ namespace MapControl UpdateTransform(false, true); } - private void ProjectionCenterPropertyChanged() - { - ResetTransformCenter(); - UpdateTransform(); - } - private Location CoerceCenterProperty(DependencyProperty property, Location center) { var c = center; @@ -754,90 +349,5 @@ namespace MapControl this.BeginAnimation(HeadingProperty, null); } } - - private void SetValueInternal(DependencyProperty property, object value) - { - internalPropertyChange = true; - - SetValue(property, value); - - internalPropertyChange = false; - } - - private void UpdateTransform(bool resetTransformCenter = false, bool projectionChanged = false) - { - var transformCenterChanged = false; - var viewScale = ViewTransform.ZoomLevelToScale(ZoomLevel); - var projection = MapProjection; - - projection.Center = ProjectionCenter ?? Center; - - var mapCenter = projection.LocationToMap(transformCenter ?? Center); - - if (mapCenter.HasValue) - { - ViewTransform.SetTransform(mapCenter.Value, viewCenter, viewScale, -Heading); - - if (transformCenter != null) - { - var center = ViewToLocation(new Point(RenderSize.Width / 2d, RenderSize.Height / 2d)); - - if (center != null) - { - var centerLatitude = center.Latitude; - var centerLongitude = Location.NormalizeLongitude(center.Longitude); - - if (centerLatitude < -maxLatitude || centerLatitude > maxLatitude) - { - centerLatitude = Math.Min(Math.Max(centerLatitude, -maxLatitude), maxLatitude); - resetTransformCenter = true; - } - - center = new Location(centerLatitude, centerLongitude); - - 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(); - - projection.Center = ProjectionCenter ?? Center; - - mapCenter = projection.LocationToMap(center); - - if (mapCenter.HasValue) - { - ViewTransform.SetTransform(mapCenter.Value, viewCenter, viewScale, -Heading); - } - } - } - } - - SetViewScale(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); - } } } diff --git a/MapControl/Shared/MapBaseCommon.cs b/MapControl/Shared/MapBaseCommon.cs new file mode 100644 index 00000000..cb9b7d78 --- /dev/null +++ b/MapControl/Shared/MapBaseCommon.cs @@ -0,0 +1,519 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// Copyright © 2024 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using System; +#if AVALONIA +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Media; +using DependencyProperty = Avalonia.AvaloniaProperty; +using UIElement = Avalonia.Controls.Control; +#elif WINUI +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +#elif UWP +using Windows.UI; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; +#else +using System.Windows; +using System.Windows.Media; +#endif + +namespace MapControl +{ + public interface IMapLayer : IMapElement + { + Brush MapBackground { get; } + Brush MapForeground { get; } + } + + /// + /// The map control. Displays map content provided by one or more tile or image layers, + /// such as MapTileLayerBase or MapImageLayer instances. + /// The visible map area is defined by the Center and ZoomLevel properties. + /// The map can be rotated by an angle that is given by the Heading property. + /// MapBase can contain map overlay child elements like other MapPanels or MapItemsControls. + /// + public partial class MapBase : MapPanel + { + public static TimeSpan ImageFadeDuration { get; set; } = TimeSpan.FromSeconds(0.1); + + public static readonly DependencyProperty ForegroundProperty = + DependencyPropertyHelper.Register(nameof(Foreground), new SolidColorBrush(Colors.Black)); + + public static readonly DependencyProperty AnimationDurationProperty = + DependencyPropertyHelper.Register(nameof(AnimationDuration), TimeSpan.FromSeconds(0.3)); + + public static readonly DependencyProperty MapLayerProperty = + DependencyPropertyHelper.Register(nameof(MapLayer), null, false, + (map, oldValue, newValue) => map.MapLayerPropertyChanged(oldValue, newValue)); + + public static readonly DependencyProperty MapProjectionProperty = + DependencyPropertyHelper.Register(nameof(MapProjection), new WebMercatorProjection(), false, + (map, oldValue, newValue) => map.MapProjectionPropertyChanged(newValue)); + + public static readonly DependencyProperty ProjectionCenterProperty = + DependencyPropertyHelper.Register(nameof(ProjectionCenter), null, false, + (map, oldValue, newValue) => map.ProjectionCenterPropertyChanged()); + + private Location transformCenter; + private Point viewCenter; + private double centerLongitude; + private double maxLatitude = 90d; + private bool internalPropertyChange; + + /// + /// Raised when the current map viewport has changed. + /// + public event EventHandler ViewportChanged; + + /// + /// Gets or sets the map foreground Brush. + /// + public Brush Foreground + { + get => (Brush)GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + /// + /// Gets or sets the Duration of the Center, ZoomLevel and Heading animations. + /// The default value is 0.3 seconds. + /// + public TimeSpan AnimationDuration + { + get => (TimeSpan)GetValue(AnimationDurationProperty); + set => SetValue(AnimationDurationProperty, value); + } + + /// + /// Gets or sets the base map layer, which is added as first element to the Children collection. + /// If the layer implements IMapLayer (like MapTileLayer or MapImageLayer), its (non-null) MapBackground + /// and MapForeground property values are used for the MapBase Background and Foreground properties. + /// + public UIElement MapLayer + { + get => (UIElement)GetValue(MapLayerProperty); + set => SetValue(MapLayerProperty, value); + } + + /// + /// Gets or sets the MapProjection used by the map control. + /// + public MapProjection MapProjection + { + get => (MapProjection)GetValue(MapProjectionProperty); + set => SetValue(MapProjectionProperty, value); + } + + /// + /// Gets or sets an optional center (reference point) for azimuthal projections. + /// If ProjectionCenter is null, the Center property value will be used instead. + /// + public Location ProjectionCenter + { + get => (Location)GetValue(ProjectionCenterProperty); + set => SetValue(ProjectionCenterProperty, value); + } + + /// + /// Gets or sets the location of the center point of the map. + /// + public Location Center + { + get => (Location)GetValue(CenterProperty); + set => SetValue(CenterProperty, value); + } + + /// + /// Gets or sets the target value of a Center animation. + /// + public Location TargetCenter + { + get => (Location)GetValue(TargetCenterProperty); + set => SetValue(TargetCenterProperty, value); + } + + /// + /// 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. + /// + public double MinZoomLevel + { + get => (double)GetValue(MinZoomLevelProperty); + set => SetValue(MinZoomLevelProperty, value); + } + + /// + /// Gets or sets the maximum value of the ZoomLevel and TargetZoomLevel properties. + /// Must not be less than MinZoomLevel. The default value is 20. + /// + public double MaxZoomLevel + { + get => (double)GetValue(MaxZoomLevelProperty); + set => SetValue(MaxZoomLevelProperty, value); + } + + /// + /// Gets or sets the map zoom level. + /// + public double ZoomLevel + { + get => (double)GetValue(ZoomLevelProperty); + set => SetValue(ZoomLevelProperty, value); + } + + /// + /// Gets or sets the target value of a ZoomLevel animation. + /// + public double TargetZoomLevel + { + get => (double)GetValue(TargetZoomLevelProperty); + set => SetValue(TargetZoomLevelProperty, value); + } + + /// + /// Gets or sets the map heading, a counter-clockwise rotation angle in degrees. + /// + public double Heading + { + get => (double)GetValue(HeadingProperty); + set => SetValue(HeadingProperty, value); + } + + /// + /// Gets or sets the target value of a Heading animation. + /// + public double TargetHeading + { + get => (double)GetValue(TargetHeadingProperty); + set => SetValue(TargetHeadingProperty, value); + } + + /// + /// Gets the ViewTransform instance that is used to transform between projected + /// map coordinates and view coordinates. + /// + public ViewTransform ViewTransform { get; } = new ViewTransform(); + + /// + /// Gets the map scale as the horizontal and vertical scaling factors from geographic + /// coordinates to view coordinates at the specified location, as pixels per meter. + /// + public Point GetScale(Location location) + { + var relativeScale = MapProjection.GetRelativeScale(location); + + return new Point(ViewTransform.Scale * relativeScale.X, ViewTransform.Scale * relativeScale.Y); + } + + /// + /// Transforms a Location in geographic coordinates to a Point in view coordinates. + /// + public Point? LocationToView(Location location) + { + var point = MapProjection.LocationToMap(location); + + if (!point.HasValue) + { + return null; + } + + return ViewTransform.MapToView(point.Value); + } + + /// + /// Transforms a Point in view coordinates to a Location in geographic coordinates. + /// + public Location ViewToLocation(Point point) + { + return MapProjection.MapToLocation(ViewTransform.ViewToMap(point)); + } + + /// + /// Transforms a Rect in view coordinates to a BoundingBox in geographic coordinates. + /// + public BoundingBox ViewRectToBoundingBox(Rect rect) + { + var p1 = ViewTransform.ViewToMap(new Point(rect.X, rect.Y)); + var p2 = ViewTransform.ViewToMap(new Point(rect.X, rect.Y + rect.Height)); + var p3 = ViewTransform.ViewToMap(new Point(rect.X + rect.Width, rect.Y)); + var p4 = ViewTransform.ViewToMap(new Point(rect.X + rect.Width, rect.Y + rect.Height)); + + var x1 = Math.Min(p1.X, Math.Min(p2.X, Math.Min(p3.X, p4.X))); + var y1 = Math.Min(p1.Y, Math.Min(p2.Y, Math.Min(p3.Y, p4.Y))); + var x2 = Math.Max(p1.X, Math.Max(p2.X, Math.Max(p3.X, p4.X))); + var y2 = Math.Max(p1.Y, Math.Max(p2.Y, Math.Max(p3.Y, p4.Y))); + + return MapProjection.MapToBoundingBox(new Rect(x1, y1, x2 - x1, y2 - y1)); + } + + /// + /// 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. + /// + public void SetTransformCenter(Point center) + { + transformCenter = ViewToLocation(center); + viewCenter = transformCenter != null ? center : new Point(RenderSize.Width / 2d, RenderSize.Height / 2d); + } + + /// + /// Resets the temporary transform center point set by SetTransformCenter. + /// + public void ResetTransformCenter() + { + transformCenter = null; + viewCenter = new Point(RenderSize.Width / 2d, RenderSize.Height / 2d); + } + + /// + /// Changes the Center property according to the specified translation in view coordinates. + /// + public void TranslateMap(Point translation) + { + if (transformCenter != null) + { + ResetTransformCenter(); + UpdateTransform(); + } + + if (translation.X != 0d || translation.Y != 0d) + { + var center = ViewToLocation(new Point(viewCenter.X - translation.X, viewCenter.Y - translation.Y)); + + if (center != null) + { + Center = center; + } + } + } + + /// + /// 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. + /// + public void TransformMap(Point center, Point translation, double rotation, double scale) + { + if (rotation != 0d || scale != 1d) + { + SetTransformCenter(center); + + viewCenter = new Point(viewCenter.X + translation.X, viewCenter.Y + translation.Y); + + if (rotation != 0d) + { + var heading = (((Heading - rotation) % 360d) + 360d) % 360d; + + SetValueInternal(HeadingProperty, heading); + SetValueInternal(TargetHeadingProperty, heading); + } + + if (scale != 1d) + { + var zoomLevel = Math.Min(Math.Max(ZoomLevel + Math.Log(scale, 2d), MinZoomLevel), MaxZoomLevel); + + SetValueInternal(ZoomLevelProperty, zoomLevel); + SetValueInternal(TargetZoomLevelProperty, zoomLevel); + } + + UpdateTransform(true); + } + else + { + // More accurate than SetTransformCenter. + // + TranslateMap(translation); + } + } + + /// + /// Sets the value of the TargetZoomLevel property while retaining the specified center point + /// in view coordinates. + /// + public void ZoomMap(Point center, double zoomLevel) + { + zoomLevel = Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel); + + if (TargetZoomLevel != zoomLevel) + { + SetTransformCenter(center); + + TargetZoomLevel = zoomLevel; + } + } + + /// + /// Sets the TargetZoomLevel and TargetCenter properties so that the specified bounding box + /// fits into the current view. The TargetHeading property is set to zero. + /// + public void ZoomToBounds(BoundingBox boundingBox) + { + var rect = MapProjection.BoundingBoxToMap(boundingBox); + + if (rect.HasValue) + { + var rectCenter = new Point(rect.Value.X + rect.Value.Width / 2d, rect.Value.Y + rect.Value.Height / 2d); + var targetCenter = MapProjection.MapToLocation(rectCenter); + + if (targetCenter != null) + { + var scale = Math.Min(RenderSize.Width / rect.Value.Width, RenderSize.Height / rect.Value.Height); + + TargetZoomLevel = ViewTransform.ScaleToZoomLevel(scale); + TargetCenter = targetCenter; + TargetHeading = 0d; + } + } + } + + internal double ConstrainedLongitude(double longitude) + { + var offset = longitude - Center.Longitude; + + if (offset > 180d) + { + longitude = Center.Longitude + (offset % 360d) - 360d; + } + else if (offset < -180d) + { + longitude = Center.Longitude + (offset % 360d) + 360d; + } + + return longitude; + } + + private void SetValueInternal(DependencyProperty property, object value) + { + internalPropertyChange = true; + + SetValue(property, value); + + internalPropertyChange = false; + } + + private void MapLayerPropertyChanged(UIElement oldLayer, UIElement newLayer) + { + if (oldLayer != null) + { + Children.Remove(oldLayer); + + if (oldLayer is IMapLayer mapLayer) + { + if (mapLayer.MapBackground != null) + { + ClearValue(BackgroundProperty); + } + if (mapLayer.MapForeground != null) + { + ClearValue(ForegroundProperty); + } + } + } + + if (newLayer != null) + { + Children.Insert(0, newLayer); + + if (newLayer is IMapLayer mapLayer) + { + if (mapLayer.MapBackground != null) + { + Background = mapLayer.MapBackground; + } + if (mapLayer.MapForeground != null) + { + Foreground = mapLayer.MapForeground; + } + } + } + } + + private void ProjectionCenterPropertyChanged() + { + ResetTransformCenter(); + UpdateTransform(); + } + + private void UpdateTransform(bool resetTransformCenter = false, bool projectionChanged = false) + { + var transformCenterChanged = false; + var viewScale = ViewTransform.ZoomLevelToScale(ZoomLevel); + var projection = MapProjection; + + projection.Center = ProjectionCenter ?? Center; + + var mapCenter = projection.LocationToMap(transformCenter ?? Center); + + if (mapCenter.HasValue) + { + ViewTransform.SetTransform(mapCenter.Value, viewCenter, viewScale, -Heading); + + if (transformCenter != null) + { + var center = ViewToLocation(new Point(RenderSize.Width / 2d, RenderSize.Height / 2d)); + + if (center != null) + { + var centerLatitude = center.Latitude; + var centerLongitude = Location.NormalizeLongitude(center.Longitude); + + if (centerLatitude < -maxLatitude || centerLatitude > maxLatitude) + { + centerLatitude = Math.Min(Math.Max(centerLatitude, -maxLatitude), maxLatitude); + resetTransformCenter = true; + } + + center = new Location(centerLatitude, centerLongitude); + + 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(); + + projection.Center = ProjectionCenter ?? Center; + + mapCenter = projection.LocationToMap(center); + + if (mapCenter.HasValue) + { + ViewTransform.SetTransform(mapCenter.Value, viewCenter, viewScale, -Heading); + } + } + } + } + + SetViewScale(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); + } + } +} diff --git a/MapControl/Shared/MapPanel.cs b/MapControl/Shared/MapPanel.cs index f68e6074..97f09c79 100644 --- a/MapControl/Shared/MapPanel.cs +++ b/MapControl/Shared/MapPanel.cs @@ -3,8 +3,14 @@ // Licensed under the Microsoft Public License (Ms-PL) using System; -using System.Linq; -#if WINUI +#if AVALONIA +using Avalonia.Controls; +using Avalonia.Media; +using DependencyProperty = Avalonia.AvaloniaProperty; +using FrameworkElement = Avalonia.Controls.Control; +using HorizontalAlignment = Avalonia.Layout.HorizontalAlignment; +using VerticalAlignment = Avalonia.Layout.VerticalAlignment; +#elif WINUI using Windows.Foundation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -20,6 +26,10 @@ using System.Windows.Controls; using System.Windows.Media; #endif +/// +/// Arranges child elements on a Map at positions specified by the attached property Location, +/// or in rectangles specified by the attached property BoundingBox. +/// namespace MapControl { /// @@ -30,19 +40,31 @@ namespace MapControl 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; - } - } + public static readonly DependencyProperty AutoCollapseProperty = + DependencyPropertyHelper.RegisterAttached("AutoCollapse"); + + public static readonly DependencyProperty LocationProperty = + DependencyPropertyHelper.RegisterAttached("Location", null, false, + (obj, oldVale, newValue) => (obj.Parent as MapPanel)?.InvalidateArrange()); + + public static readonly DependencyProperty BoundingBoxProperty = + DependencyPropertyHelper.RegisterAttached("BoundingBox", null, false, + (obj, oldVale, newValue) => (obj.Parent as MapPanel)?.InvalidateArrange()); + + private static readonly DependencyProperty ViewPositionProperty = + DependencyPropertyHelper.RegisterAttached("ViewPosition"); + + private static readonly DependencyProperty ParentMapProperty = + DependencyPropertyHelper.RegisterAttached("ParentMap", null, true, + (obj, oldVale, newValue) => + { + if (obj is IMapElement mapElement) + { + mapElement.ParentMap = newValue; + } + }); private MapBase parentMap; @@ -55,9 +77,6 @@ namespace MapControl 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. @@ -115,6 +134,16 @@ namespace MapControl return (Point?)element.GetValue(ViewPositionProperty); } + /// + /// 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. + /// + protected virtual void SetViewPosition(FrameworkElement element, ref Point? position) + { + element.SetValue(ViewPositionProperty, position); + } + protected virtual void SetParentMap(MapBase map) { if (parentMap != null && parentMap != this) @@ -146,7 +175,7 @@ namespace MapControl { availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity); - foreach (var element in Children.OfType()) + foreach (var element in ChildElements) { element.Measure(availableSize); } @@ -158,7 +187,7 @@ namespace MapControl { if (parentMap != null) { - foreach (var element in Children.OfType()) + foreach (var element in ChildElements) { var location = GetLocation(element); var position = location != null ? GetViewPosition(location) : null; @@ -167,8 +196,7 @@ namespace MapControl if (GetAutoCollapse(element)) { - element.Visibility = position.HasValue && IsOutsideViewport(position.Value) - ? Visibility.Collapsed : Visibility.Visible; + SetVisible(element, !(position.HasValue && IsOutsideViewport(position.Value))); } if (position.HasValue) @@ -230,11 +258,11 @@ namespace MapControl { var rectCenter = new Point(rect.X + rect.Width / 2d, rect.Y + rect.Height / 2d); var position = parentMap.ViewTransform.MapToView(rectCenter); + var projection = parentMap.MapProjection; - if (parentMap.MapProjection.Type <= MapProjectionType.NormalCylindrical && - IsOutsideViewport(position)) + if (projection.Type <= MapProjectionType.NormalCylindrical && IsOutsideViewport(position)) { - var location = parentMap.MapProjection.MapToLocation(rectCenter); + var location = projection.MapToLocation(rectCenter); if (location != null) { @@ -359,9 +387,7 @@ namespace MapControl } else if (rect.Rotation != 0d) { - rotateTransform = new RotateTransform { Angle = rect.Rotation }; - element.RenderTransform = rotateTransform; - element.RenderTransformOrigin = new Point(0.5, 0.5); + SetRenderTransform(element, new RotateTransform { Angle = rect.Rotation }, 0.5, 0.5); } } @@ -375,7 +401,7 @@ namespace MapControl element.Arrange(rect); } - internal static Size GetDesiredSize(UIElement element) + internal static Size GetDesiredSize(FrameworkElement element) { var width = 0d; var height = 0d; diff --git a/MapControl/Shared/MapTileLayer.cs b/MapControl/Shared/MapTileLayer.cs index b50ea72a..c8beb995 100644 --- a/MapControl/Shared/MapTileLayer.cs +++ b/MapControl/Shared/MapTileLayer.cs @@ -4,7 +4,10 @@ using System; using System.Threading.Tasks; -#if WINUI +#if AVALONIA +using Avalonia.Media; +using DependencyProperty = Avalonia.AvaloniaProperty; +#elif WINUI using Windows.Foundation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; @@ -24,6 +27,15 @@ namespace MapControl /// public class MapTileLayer : MapTileLayerBase { + public static readonly DependencyProperty MinZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(MinZoomLevel), 0); + + public static readonly DependencyProperty MaxZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(MaxZoomLevel), 19); + + public static readonly DependencyProperty ZoomLevelOffsetProperty = + DependencyPropertyHelper.Register(nameof(ZoomLevelOffset), 0d); + private const int TileSize = 256; private static readonly Point MapTopLeft = new Point( @@ -39,15 +51,6 @@ namespace MapControl Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)" }; - public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register( - nameof(MinZoomLevel), typeof(int), typeof(MapTileLayer), new PropertyMetadata(0)); - - public static readonly DependencyProperty MaxZoomLevelProperty = DependencyProperty.Register( - nameof(MaxZoomLevel), typeof(int), typeof(MapTileLayer), new PropertyMetadata(19)); - - public static readonly DependencyProperty ZoomLevelOffsetProperty = DependencyProperty.Register( - nameof(ZoomLevelOffset), typeof(double), typeof(MapTileLayer), new PropertyMetadata(0d)); - public TileMatrix TileMatrix { get; private set; } public TileCollection Tiles { get; private set; } = new TileCollection(); diff --git a/MapControl/Shared/MapTileLayerBase.cs b/MapControl/Shared/MapTileLayerBase.cs index 9f39d500..b4325c4a 100644 --- a/MapControl/Shared/MapTileLayerBase.cs +++ b/MapControl/Shared/MapTileLayerBase.cs @@ -5,7 +5,12 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -#if WINUI +#if AVALONIA +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Threading; +using DependencyProperty = Avalonia.AvaloniaProperty; +#elif WINUI using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; @@ -23,36 +28,35 @@ using System.Windows.Threading; namespace MapControl { - public abstract class MapTileLayerBase : Panel, IMapLayer + public abstract partial class MapTileLayerBase : Panel, IMapLayer { - public static readonly DependencyProperty TileSourceProperty = DependencyProperty.Register( - nameof(TileSource), typeof(TileSource), typeof(MapTileLayerBase), - new PropertyMetadata(null, async (o, e) => await ((MapTileLayerBase)o).Update(true))); + public static readonly DependencyProperty TileSourceProperty = + DependencyPropertyHelper.Register(nameof(TileSource), null, false, + async (obj, oldVale, newValue) => await obj.Update(true)); - public static readonly DependencyProperty SourceNameProperty = DependencyProperty.Register( - nameof(SourceName), typeof(string), typeof(MapTileLayerBase), new PropertyMetadata(null)); + public static readonly DependencyProperty SourceNameProperty = + DependencyPropertyHelper.Register(nameof(SourceName)); - public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register( - nameof(Description), typeof(string), typeof(MapTileLayerBase), new PropertyMetadata(null)); + public static readonly DependencyProperty DescriptionProperty = + DependencyPropertyHelper.Register(nameof(Description)); - public static readonly DependencyProperty MaxBackgroundLevelsProperty = DependencyProperty.Register( - nameof(MaxBackgroundLevels), typeof(int), typeof(MapTileLayerBase), new PropertyMetadata(5)); + public static readonly DependencyProperty MaxBackgroundLevelsProperty = + DependencyPropertyHelper.Register(nameof(MaxBackgroundLevels), 5); - public static readonly DependencyProperty UpdateIntervalProperty = DependencyProperty.Register( - nameof(UpdateInterval), typeof(TimeSpan), typeof(MapTileLayerBase), - new PropertyMetadata(TimeSpan.FromSeconds(0.2), (o, e) => ((MapTileLayerBase)o).updateTimer.Interval = (TimeSpan)e.NewValue)); + public static readonly DependencyProperty UpdateIntervalProperty = + DependencyPropertyHelper.Register(nameof(UpdateInterval), TimeSpan.FromSeconds(0.2)); - public static readonly DependencyProperty UpdateWhileViewportChangingProperty = DependencyProperty.Register( - nameof(UpdateWhileViewportChanging), typeof(bool), typeof(MapTileLayerBase), new PropertyMetadata(false)); + public static readonly DependencyProperty UpdateWhileViewportChangingProperty = + DependencyPropertyHelper.Register(nameof(UpdateWhileViewportChanging)); - public static readonly DependencyProperty MapBackgroundProperty = DependencyProperty.Register( - nameof(MapBackground), typeof(Brush), typeof(MapTileLayerBase), new PropertyMetadata(null)); + public static readonly DependencyProperty MapBackgroundProperty = + DependencyPropertyHelper.Register(nameof(MapBackground)); - public static readonly DependencyProperty MapForegroundProperty = DependencyProperty.Register( - nameof(MapForeground), typeof(Brush), typeof(MapTileLayerBase), new PropertyMetadata(null)); + public static readonly DependencyProperty MapForegroundProperty = + DependencyPropertyHelper.Register(nameof(MapForeground)); - public static readonly DependencyProperty LoadingProgressProperty = DependencyProperty.Register( - nameof(LoadingProgress), typeof(double), typeof(MapTileLayerBase), new PropertyMetadata(1d)); + public static readonly DependencyProperty LoadingProgressProperty = + DependencyPropertyHelper.Register(nameof(LoadingProgress), 1d); private readonly Progress loadingProgress; private readonly DispatcherTimer updateTimer; @@ -61,9 +65,9 @@ namespace MapControl protected MapTileLayerBase() { - RenderTransform = new MatrixTransform(); + MapPanel.SetRenderTransform(this, new MatrixTransform()); - loadingProgress = new Progress(p => LoadingProgress = p); + loadingProgress = new Progress(p => SetValue(LoadingProgressProperty, p)); updateTimer = this.CreateTimer(UpdateInterval); updateTimer.Tick += async (s, e) => await Update(false); @@ -155,11 +159,7 @@ namespace MapControl /// /// Gets the progress of the TileImageLoader as a double value between 0 and 1. /// - public double LoadingProgress - { - get => (double)GetValue(LoadingProgressProperty); - private set => SetValue(LoadingProgressProperty, value); - } + public double LoadingProgress => (double)GetValue(LoadingProgressProperty); /// /// Implements IMapElement.ParentMap. diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index bec8a391..c5e10f6f 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -68,6 +68,9 @@ CenteredBoundingBox.cs + + DependencyPropertyHelper.cs + EquirectangularProjection.cs @@ -104,6 +107,9 @@ MapBase.cs + + MapBaseCommon.cs + MapBorderPanel.cs diff --git a/MapControl/WPF/MapBase.WPF.cs b/MapControl/WPF/MapBase.WPF.cs index 5ce324c3..2942efca 100644 --- a/MapControl/WPF/MapBase.WPF.cs +++ b/MapControl/WPF/MapBase.WPF.cs @@ -3,15 +3,11 @@ // Licensed under the Microsoft Public License (Ms-PL) using System.Windows; -using System.Windows.Controls; namespace MapControl { public partial class MapBase { - public static readonly DependencyProperty ForegroundProperty = - Control.ForegroundProperty.AddOwner(typeof(MapBase)); - public static readonly DependencyProperty CenterProperty = DependencyProperty.Register( nameof(Center), typeof(Location), typeof(MapBase), new FrameworkPropertyMetadata( new Location(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, diff --git a/MapControl/WPF/MapPanel.WPF.cs b/MapControl/WPF/MapPanel.WPF.cs index 094027e8..19cc3d82 100644 --- a/MapControl/WPF/MapPanel.WPF.cs +++ b/MapControl/WPF/MapPanel.WPF.cs @@ -2,35 +2,20 @@ // Copyright © 2024 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) +using System.Collections.Generic; +using System.Linq; using System.Windows; +using System.Windows.Media; namespace MapControl { public partial class MapPanel { - public static readonly DependencyProperty LocationProperty = DependencyProperty.RegisterAttached( - "Location", typeof(Location), typeof(MapPanel), - new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsParentArrange)); - - public static readonly DependencyProperty BoundingBoxProperty = DependencyProperty.RegisterAttached( - "BoundingBox", typeof(BoundingBox), typeof(MapPanel), - new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsParentArrange)); - - private static readonly DependencyPropertyKey ParentMapPropertyKey = DependencyProperty.RegisterAttachedReadOnly( - "ParentMap", typeof(MapBase), typeof(MapPanel), - new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, ParentMapPropertyChanged)); - - private static readonly DependencyPropertyKey ViewPositionPropertyKey = DependencyProperty.RegisterAttachedReadOnly( - "ViewPosition", typeof(Point?), typeof(MapPanel), new PropertyMetadata()); - - public static readonly DependencyProperty ParentMapProperty = ParentMapPropertyKey.DependencyProperty; - public static readonly DependencyProperty ViewPositionProperty = ViewPositionPropertyKey.DependencyProperty; - public MapPanel() { if (this is MapBase) { - SetValue(ParentMapPropertyKey, this); + SetValue(ParentMapProperty, this); } } @@ -39,14 +24,17 @@ namespace MapControl return (MapBase)element.GetValue(ParentMapProperty); } - /// - /// 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. - /// - protected virtual void SetViewPosition(FrameworkElement element, ref Point? position) + public static void SetRenderTransform(FrameworkElement element, Transform transform, double originX = 0d, double originY = 0d) { - element.SetValue(ViewPositionPropertyKey, position); + element.RenderTransform = transform; + element.RenderTransformOrigin = new Point(originX, originY); + } + + private IEnumerable ChildElements => Children.OfType(); + + private static void SetVisible(FrameworkElement element, bool visible) + { + element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; } } } diff --git a/MapControl/WPF/Timer.WPF.cs b/MapControl/WPF/Timer.WPF.cs index f99d1732..10a64c79 100644 --- a/MapControl/WPF/Timer.WPF.cs +++ b/MapControl/WPF/Timer.WPF.cs @@ -3,10 +3,11 @@ // Licensed under the Microsoft Public License (Ms-PL) using System; -#if UWP +#if AVALONIA +using Avalonia.Threading; +#elif UWP using Windows.UI.Xaml; #else -using System.Windows; using System.Windows.Threading; #endif @@ -14,14 +15,9 @@ namespace MapControl { internal static class Timer { - public static DispatcherTimer CreateTimer(this DependencyObject obj, TimeSpan interval) + public static DispatcherTimer CreateTimer(this object _, TimeSpan interval) { - var timer = new DispatcherTimer - { - Interval = interval - }; - - return timer; + return new DispatcherTimer { Interval = interval }; } public static void Run(this DispatcherTimer timer, bool restart = false) diff --git a/MapControl/WinUI/MapBase.WinUI.cs b/MapControl/WinUI/MapBase.WinUI.cs index 38310015..33dd65be 100644 --- a/MapControl/WinUI/MapBase.WinUI.cs +++ b/MapControl/WinUI/MapBase.WinUI.cs @@ -16,9 +16,6 @@ namespace MapControl { public partial class MapBase { - public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register( - nameof(Foreground), typeof(Brush), typeof(MapBase), new PropertyMetadata(new SolidColorBrush(Colors.Black))); - public static readonly DependencyProperty CenterProperty = DependencyProperty.Register( nameof(Center), typeof(Location), typeof(MapBase), new PropertyMetadata(new Location(), (o, e) => ((MapBase)o).CenterPropertyChanged((Location)e.NewValue))); diff --git a/MapControl/WinUI/MapPanel.WinUI.cs b/MapControl/WinUI/MapPanel.WinUI.cs index f511d4c2..734d65ae 100644 --- a/MapControl/WinUI/MapPanel.WinUI.cs +++ b/MapControl/WinUI/MapPanel.WinUI.cs @@ -2,6 +2,8 @@ // Copyright © 2024 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) +using System.Collections.Generic; +using System.Linq; #if WINUI using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; @@ -14,20 +16,6 @@ namespace MapControl { public partial class MapPanel { - public static readonly DependencyProperty LocationProperty = DependencyProperty.RegisterAttached( - "Location", typeof(Location), typeof(MapPanel), - new PropertyMetadata(null, (o, e) => (((FrameworkElement)o).Parent as MapPanel)?.InvalidateArrange())); - - public static readonly DependencyProperty BoundingBoxProperty = DependencyProperty.RegisterAttached( - "BoundingBox", typeof(BoundingBox), typeof(MapPanel), - new PropertyMetadata(null, (o, e) => (((FrameworkElement)o).Parent as MapPanel)?.InvalidateArrange())); - - public static readonly DependencyProperty ParentMapProperty = DependencyProperty.RegisterAttached( - "ParentMap", typeof(MapBase), typeof(MapPanel), new PropertyMetadata(null, ParentMapPropertyChanged)); - - private static readonly DependencyProperty ViewPositionProperty = DependencyProperty.RegisterAttached( - "ViewPosition", typeof(Point?), typeof(MapPanel), new PropertyMetadata(null)); - public MapPanel() { InitMapElement(this); @@ -53,6 +41,8 @@ namespace MapControl { var parentMap = (MapBase)element.GetValue(ParentMapProperty); + // Traverse visual tree because of missing property value inheritance. + if (parentMap == null && VisualTreeHelper.GetParent(element) is FrameworkElement parentElement) { @@ -67,14 +57,17 @@ namespace MapControl return parentMap; } - /// - /// 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. - /// - protected virtual void SetViewPosition(FrameworkElement element, ref Point? position) + public static void SetRenderTransform(FrameworkElement element, Transform transform, double originX = 0d, double originY = 0d) { - element.SetValue(ViewPositionProperty, position); + element.RenderTransform = transform; + element.RenderTransformOrigin = new Point(originX, originY); + } + + private IEnumerable ChildElements => Children.OfType(); + + private static void SetVisible(FrameworkElement element, bool visible) + { + element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; } } }