From 74f4e0176b842ec18e271be669a801157670223d Mon Sep 17 00:00:00 2001 From: ClemensFischer Date: Tue, 21 May 2024 13:51:10 +0200 Subject: [PATCH] MapBase dependency properties --- .../DependencyPropertyHelper.Avalonia.cs | 34 +- .../Avalonia/LocationAnimator.Avalonia.cs | 1 + MapControl/Avalonia/Map.Avalonia.cs | 4 +- MapControl/Avalonia/MapBase.Avalonia.cs | 149 ++-- .../Avalonia/MapControl.Avalonia.csproj | 2 +- MapControl/Shared/MapBase.cs | 784 +++++++++++------- MapControl/Shared/MapBaseCommon.cs | 519 ------------ MapControl/UWP/MapControl.UWP.csproj | 9 +- .../DependencyPropertyHelper.WPF.cs} | 47 +- MapControl/WPF/LocationAnimation.cs | 43 + MapControl/WPF/MapBase.WPF.cs | 282 ++++++- MapControl/WinUI/Animatable.WinUI.cs | 44 +- .../WinUI/DependencyPropertyHelper.WinUI.cs | 44 + MapControl/WinUI/MapBase.WinUI.cs | 337 +++++++- 14 files changed, 1262 insertions(+), 1037 deletions(-) delete mode 100644 MapControl/Shared/MapBaseCommon.cs rename MapControl/{Shared/DependencyPropertyHelper.cs => WPF/DependencyPropertyHelper.WPF.cs} (55%) create mode 100644 MapControl/WPF/LocationAnimation.cs create mode 100644 MapControl/WinUI/DependencyPropertyHelper.WinUI.cs diff --git a/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs b/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs index d4333b06..fbabc9e5 100644 --- a/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs +++ b/MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs @@ -7,41 +7,49 @@ namespace MapControl [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001")] public static class DependencyPropertyHelper { - public static AvaloniaProperty Register( + public static StyledProperty Register( string name, TValue defaultValue = default, bool bindTwoWayByDefault = false, - Action propertyChanged = null) + Action changed = null, + Func coerce = null) where TOwner : AvaloniaObject { - StyledProperty property = AvaloniaProperty.Register(name, defaultValue, false, - bindTwoWayByDefault ? Avalonia.Data.BindingMode.TwoWay : Avalonia.Data.BindingMode.OneWay); + var property = AvaloniaProperty.Register(name, defaultValue, false, + bindTwoWayByDefault ? Avalonia.Data.BindingMode.TwoWay : Avalonia.Data.BindingMode.OneWay, null, + coerce != null ? ((obj, value) => coerce((TOwner)obj, value)) : null); - if (propertyChanged != null) + if (changed != null) { - property.Changed.AddClassHandler( - (o, e) => propertyChanged(o, e.OldValue.Value, e.NewValue.Value)); + property.Changed.AddClassHandler((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value)); } return property; } - public static AvaloniaProperty RegisterAttached( + public static AttachedProperty RegisterAttached( string name, TValue defaultValue = default, bool inherits = false, - Action propertyChanged = null) + Action changed = null) where TOwner : AvaloniaObject { - AttachedProperty property = AvaloniaProperty.RegisterAttached(name, defaultValue, inherits); + var property = AvaloniaProperty.RegisterAttached(name, defaultValue, inherits); - if (propertyChanged != null) + if (changed != null) { - property.Changed.AddClassHandler( - (o, e) => propertyChanged(o, e.OldValue.Value, e.NewValue.Value)); + property.Changed.AddClassHandler((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value)); } return property; } + + public static DirectProperty RegisterReadOnly( + string name, + Func getter) + where TOwner : AvaloniaObject + { + return AvaloniaProperty.RegisterDirect(name, getter); + } } } diff --git a/MapControl/Avalonia/LocationAnimator.Avalonia.cs b/MapControl/Avalonia/LocationAnimator.Avalonia.cs index 7a422131..ad58051f 100644 --- a/MapControl/Avalonia/LocationAnimator.Avalonia.cs +++ b/MapControl/Avalonia/LocationAnimator.Avalonia.cs @@ -10,6 +10,7 @@ namespace MapControl { public override Location Interpolate(double progress, Location oldValue, Location newValue) { + System.Diagnostics.Debug.WriteLine(progress); return new Location( (1d - progress) * oldValue.Latitude + progress * newValue.Latitude, (1d - progress) * oldValue.Longitude + progress * newValue.Longitude); diff --git a/MapControl/Avalonia/Map.Avalonia.cs b/MapControl/Avalonia/Map.Avalonia.cs index 0bd8aec6..0cbd70c7 100644 --- a/MapControl/Avalonia/Map.Avalonia.cs +++ b/MapControl/Avalonia/Map.Avalonia.cs @@ -11,8 +11,8 @@ namespace MapControl /// public class Map : MapBase { - public static readonly StyledProperty MouseWheelZoomDeltaProperty - = AvaloniaProperty.Register(nameof(MouseWheelZoomDelta), 0.25); + public static readonly StyledProperty MouseWheelZoomDeltaProperty = + DependencyPropertyHelper.Register(nameof(MouseWheelZoomDelta), 0.25); private Point? mousePosition; diff --git a/MapControl/Avalonia/MapBase.Avalonia.cs b/MapControl/Avalonia/MapBase.Avalonia.cs index d21ce5d5..6b7a05da 100644 --- a/MapControl/Avalonia/MapBase.Avalonia.cs +++ b/MapControl/Avalonia/MapBase.Avalonia.cs @@ -5,9 +5,7 @@ global using Avalonia; using Avalonia.Animation; using Avalonia.Controls; -using Avalonia.Data; using Avalonia.Styling; -using System; using System.Threading; using System.Threading.Tasks; @@ -15,40 +13,48 @@ namespace MapControl { public partial class MapBase { - 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 CenterProperty = + DependencyPropertyHelper.Register(nameof(Center), new Location(), true, + (map, oldValue, newValue) => map.CenterPropertyChanged(newValue), + (map, value) => map.CoerceCenterProperty(value)); - 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 TargetCenterProperty = + DependencyPropertyHelper.Register(nameof(TargetCenter), new Location(), true, + async (map, oldValue, newValue) => await map.TargetCenterPropertyChanged(newValue), + (map, value) => map.CoerceCenterProperty(value)); - public static readonly StyledProperty MinZoomLevelProperty - = AvaloniaProperty.Register(nameof(MinZoomLevel), 1d, false, - BindingMode.OneWay, null, (map, minZoomLevel) => ((MapBase)map).CoerceMinZoomLevelProperty(minZoomLevel)); + public static readonly StyledProperty MinZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(MinZoomLevel), 1d, false, + (map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceMinZoomLevelProperty(value)); - public static readonly StyledProperty MaxZoomLevelProperty - = AvaloniaProperty.Register(nameof(MaxZoomLevel), 20d, false, - BindingMode.OneWay, null, (map, maxZoomLevel) => ((MapBase)map).CoerceMaxZoomLevelProperty(maxZoomLevel)); + public static readonly StyledProperty MaxZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(MaxZoomLevel), 20d, false, + (map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceMinZoomLevelProperty(value)); - public static readonly StyledProperty ZoomLevelProperty - = AvaloniaProperty.Register(nameof(ZoomLevel), 1d, false, - BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel)); + public static readonly StyledProperty ZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(ZoomLevel), 1d, true, + (map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceZoomLevelProperty(value)); - public static readonly StyledProperty TargetZoomLevelProperty - = AvaloniaProperty.Register(nameof(TargetZoomLevel), 1d, false, - BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel)); + public static readonly StyledProperty TargetZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(TargetZoomLevel), 1d, true, + async (map, oldValue, newValue) => await map.TargetZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceZoomLevelProperty(value)); - public static readonly StyledProperty HeadingProperty - = AvaloniaProperty.Register(nameof(Heading), 0d, false, - BindingMode.TwoWay, null, (map, heading) => CoerceHeadingProperty(heading)); + public static readonly StyledProperty HeadingProperty = + DependencyPropertyHelper.Register(nameof(Heading), 0d, true, + (map, oldValue, newValue) => map.HeadingPropertyChanged(newValue), + (map, value) => map.CoerceHeadingProperty(value)); - public static readonly StyledProperty TargetHeadingProperty - = AvaloniaProperty.Register(nameof(TargetHeading), 0d, false, - BindingMode.TwoWay, null, (map, heading) => CoerceHeadingProperty(heading)); + public static readonly StyledProperty TargetHeadingProperty = + DependencyPropertyHelper.Register(nameof(TargetHeading), 0d, true, + async (map, oldValue, newValue) => await map.TargetHeadingPropertyChanged(newValue), + (map, value) => map.CoerceHeadingProperty(value)); - public static readonly DirectProperty ViewScaleProperty - = AvaloniaProperty.RegisterDirect(nameof(ViewScale), map => map.ViewScale); + public static readonly DirectProperty ViewScaleProperty = + DependencyPropertyHelper.RegisterReadOnly(nameof(ViewScale), map => map.ViewScale); private CancellationTokenSource centerCts; private CancellationTokenSource zoomLevelCts; @@ -59,27 +65,9 @@ namespace MapControl static MapBase() { - Animation.RegisterCustomAnimator(); - ClipToBoundsProperty.OverrideDefaultValue(typeof(MapBase), true); - CenterProperty.Changed.AddClassHandler( - (map, args) => map.CenterPropertyChanged(args.NewValue.Value)); - - TargetCenterProperty.Changed.AddClassHandler( - async (map, args) => await map.TargetCenterPropertyChanged(args.NewValue.Value)); - - ZoomLevelProperty.Changed.AddClassHandler( - (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.HeadingPropertyChanged(args.NewValue.Value)); - - TargetHeadingProperty.Changed.AddClassHandler( - async (map, args) => await map.TargetHeadingPropertyChanged(args.NewValue.Value)); + Animation.RegisterCustomAnimator(); } public MapBase() @@ -123,44 +111,6 @@ namespace MapControl .Append(Matrix.CreateRotation(ViewTransform.Rotation)); } - private void MapProjectionPropertyChanged(MapProjection projection) - { - maxLatitude = 90d; - - if (projection.Type <= MapProjectionType.NormalCylindrical) - { - var maxLocation = projection.MapToLocation(new Point(0d, 180d * MapProjection.Wgs84MeterPerDegree)); - - if (maxLocation != null && maxLocation.Latitude < 90d) - { - maxLatitude = maxLocation.Latitude; - - Center = CoerceCenterProperty(Center); - } - } - - ResetTransformCenter(); - UpdateTransform(false, true); - } - - private Location CoerceCenterProperty(Location center) - { - if (center == null) - { - center = new Location(); - } - else if ( - center.Latitude < -maxLatitude || center.Latitude > maxLatitude || - center.Longitude < -180d || center.Longitude > 180d) - { - center = new Location( - Math.Min(Math.Max(center.Latitude, -maxLatitude), maxLatitude), - Location.NormalizeLongitude(center.Longitude)); - } - - return center; - } - private void CenterPropertyChanged(Location center) { if (!internalPropertyChange) @@ -204,19 +154,20 @@ namespace MapControl } } - private double CoerceMinZoomLevelProperty(double minZoomLevel) + private void MinZoomLevelPropertyChanged(double minZoomLevel) { - return Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel); + if (ZoomLevel < minZoomLevel) + { + ZoomLevel = minZoomLevel; + } } - private double CoerceMaxZoomLevelProperty(double maxZoomLevel) + private void MaxZoomLevelPropertyChanged(double maxZoomLevel) { - return Math.Max(maxZoomLevel, MinZoomLevel); - } - - private double CoerceZoomLevelProperty(double zoomLevel) - { - return Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel); + if (ZoomLevel > maxZoomLevel) + { + ZoomLevel = maxZoomLevel; + } } private void ZoomLevelPropertyChanged(double zoomLevel) @@ -256,17 +207,17 @@ namespace MapControl await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token); + if (!zoomLevelCts.IsCancellationRequested) + { + UpdateTransform(true); + } + zoomLevelCts.Dispose(); zoomLevelCts = null; zoomLevelAnimation = null; } } - private static double CoerceHeadingProperty(double heading) - { - return ((heading % 360d) + 360d) % 360d; - } - private void HeadingPropertyChanged(double heading) { if (!internalPropertyChange) diff --git a/MapControl/Avalonia/MapControl.Avalonia.csproj b/MapControl/Avalonia/MapControl.Avalonia.csproj index 17935f99..c1de38d4 100644 --- a/MapControl/Avalonia/MapControl.Avalonia.csproj +++ b/MapControl/Avalonia/MapControl.Avalonia.csproj @@ -22,7 +22,7 @@ - + diff --git a/MapControl/Shared/MapBase.cs b/MapControl/Shared/MapBase.cs index 95f3bb17..ad677442 100644 --- a/MapControl/Shared/MapBase.cs +++ b/MapControl/Shared/MapBase.cs @@ -3,66 +3,473 @@ // Licensed under the Microsoft Public License (Ms-PL) using System; -#if WINUI +#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.Animation; +using Microsoft.UI.Xaml.Media; #elif UWP +using Windows.UI; using Windows.UI.Xaml; -using Windows.UI.Xaml.Media.Animation; +using Windows.UI.Xaml.Media; #else using System.Windows; using System.Windows.Media; -using System.Windows.Media.Animation; #endif namespace MapControl { - public partial class MapBase + public interface IMapLayer : IMapElement { - public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register( - nameof(MinZoomLevel), typeof(double), typeof(MapBase), - new PropertyMetadata(1d, (o, e) => ((MapBase)o).MinZoomLevelPropertyChanged((double)e.NewValue))); + Brush MapBackground { get; } + Brush MapForeground { get; } + } - public static readonly DependencyProperty MaxZoomLevelProperty = DependencyProperty.Register( - nameof(MaxZoomLevel), typeof(double), typeof(MapBase), - new PropertyMetadata(20d, (o, e) => ((MapBase)o).MaxZoomLevelPropertyChanged((double)e.NewValue))); + /// + /// 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 AnimationEasingFunctionProperty = DependencyProperty.Register( - nameof(AnimationEasingFunction), typeof(EasingFunctionBase), typeof(MapBase), - new PropertyMetadata(new QuadraticEase { EasingMode = EasingMode.EaseOut })); + public static readonly DependencyProperty ForegroundProperty = + DependencyPropertyHelper.Register(nameof(Foreground), new SolidColorBrush(Colors.Black)); - private PointAnimation centerAnimation; - private DoubleAnimation zoomLevelAnimation; - private DoubleAnimation headingAnimation; + 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; /// - /// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations. - /// The default value is a QuadraticEase with EasingMode.EaseOut. + /// Raised when the current map viewport has changed. /// - public EasingFunctionBase AnimationEasingFunction + public event EventHandler ViewportChanged; + + /// + /// Gets or sets the map foreground Brush. + /// + public Brush Foreground { - get => (EasingFunctionBase)GetValue(AnimationEasingFunctionProperty); - set => SetValue(AnimationEasingFunctionProperty, value); + get => (Brush)GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); } /// - /// Gets the scaling factor from projected map coordinates to view coordinates, - /// as pixels per meter. + /// Gets or sets the Duration of the Center, ZoomLevel and Heading animations. + /// The default value is 0.3 seconds. /// - public double ViewScale => (double)GetValue(ViewScaleProperty); + public TimeSpan AnimationDuration + { + get => (TimeSpan)GetValue(AnimationDurationProperty); + set => SetValue(AnimationDurationProperty, value); + } /// - /// Gets a transform Matrix for scaling and rotating objects that are anchored - /// at a Location from map coordinates (i.e. meters) to view coordinates. + /// 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 Matrix GetMapTransform(Location location) + public UIElement MapLayer { - var scale = GetScale(location); + get => (UIElement)GetValue(MapLayerProperty); + set => SetValue(MapLayerProperty, value); + } - var transform = new Matrix(scale.X, 0d, 0d, scale.Y, 0d, 0d); - transform.Rotate(ViewTransform.Rotation); + /// + /// Gets or sets the MapProjection used by the map control. + /// + public MapProjection MapProjection + { + get => (MapProjection)GetValue(MapProjectionProperty); + set => SetValue(MapProjectionProperty, value); + } - return transform; + /// + /// 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 = CoerceZoomLevelProperty(zoomLevel); + + 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 Location CoerceCenterProperty(Location center) + { + if (center == null) + { + center = new Location(); + } + else if ( + center.Latitude < -maxLatitude || center.Latitude > maxLatitude || + center.Longitude < -180d || center.Longitude > 180d) + { + center = new Location( + Math.Min(Math.Max(center.Latitude, -maxLatitude), maxLatitude), + Location.NormalizeLongitude(center.Longitude)); + } + + return center; + } + + private double CoerceMinZoomLevelProperty(double minZoomLevel) + { + return Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel); + } + + private double CoerceMaxZoomLevelProperty(double maxZoomLevel) + { + return Math.Max(maxZoomLevel, MinZoomLevel); + } + + private double CoerceZoomLevelProperty(double zoomLevel) + { + return Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel); + } + + private double CoerceHeadingProperty(double heading) + { + return ((heading % 360d) + 360d) % 360d; + } + + private void SetValueInternal(DependencyProperty property, object value) + { + internalPropertyChange = true; + + SetValue(property, value); + + internalPropertyChange = false; + } + + private void 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) @@ -77,7 +484,7 @@ namespace MapControl { maxLatitude = maxLocation.Latitude; - CoerceCenterProperty(CenterProperty, Center); + Center = CoerceCenterProperty(Center); } } @@ -85,269 +492,86 @@ namespace MapControl UpdateTransform(false, true); } - private Location CoerceCenterProperty(DependencyProperty property, Location center) + private void ProjectionCenterPropertyChanged() { - var c = center; - - if (center == null) - { - center = new Location(); - } - else if ( - center.Latitude < -maxLatitude || center.Latitude > maxLatitude || - center.Longitude < -180d || center.Longitude > 180d) - { - center = new Location( - Math.Min(Math.Max(center.Latitude, -maxLatitude), maxLatitude), - Location.NormalizeLongitude(center.Longitude)); - } - - if (center != c) - { - SetValueInternal(property, center); - } - - return center; + ResetTransformCenter(); + UpdateTransform(); } - private void CenterPropertyChanged(Location center) + private void UpdateTransform(bool resetTransformCenter = false, bool projectionChanged = false) { - if (!internalPropertyChange) + var transformCenterChanged = false; + var viewScale = ViewTransform.ZoomLevelToScale(ZoomLevel); + var projection = MapProjection; + + projection.Center = ProjectionCenter ?? Center; + + var mapCenter = projection.LocationToMap(transformCenter ?? Center); + + if (mapCenter.HasValue) { - center = CoerceCenterProperty(CenterProperty, center); + ViewTransform.SetTransform(mapCenter.Value, viewCenter, viewScale, -Heading); - UpdateTransform(); - - if (centerAnimation == null) + if (transformCenter != null) { - SetValueInternal(TargetCenterProperty, center); - } - } - } + var center = ViewToLocation(new Point(RenderSize.Width / 2d, RenderSize.Height / 2d)); - private void TargetCenterPropertyChanged(Location targetCenter) - { - if (!internalPropertyChange) - { - targetCenter = CoerceCenterProperty(TargetCenterProperty, targetCenter); - - if (!targetCenter.Equals(Center)) - { - if (centerAnimation != null) + if (center != null) { - centerAnimation.Completed -= CenterAnimationCompleted; + 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); + } + } } - - centerAnimation = new PointAnimation - { - From = new Point(Center.Longitude, Center.Latitude), - To = new Point(ConstrainedLongitude(targetCenter.Longitude), targetCenter.Latitude), - Duration = AnimationDuration, - EasingFunction = AnimationEasingFunction - }; - - centerAnimation.Completed += CenterAnimationCompleted; - - this.BeginAnimation(CenterPointProperty, centerAnimation); } + + 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)); } } - private void CenterAnimationCompleted(object sender, object e) + protected override void OnViewportChanged(ViewportChangedEventArgs e) { - if (centerAnimation != null) - { - centerAnimation.Completed -= CenterAnimationCompleted; - centerAnimation = null; + base.OnViewportChanged(e); - this.BeginAnimation(CenterPointProperty, null); - } - } - - private void CenterPointPropertyChanged(Location center) - { - if (centerAnimation != null) - { - SetValueInternal(CenterProperty, center); - UpdateTransform(); - } - } - - private void MinZoomLevelPropertyChanged(double minZoomLevel) - { - if (minZoomLevel < 0d || minZoomLevel > MaxZoomLevel) - { - minZoomLevel = Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel); - - SetValueInternal(MinZoomLevelProperty, minZoomLevel); - } - - if (ZoomLevel < minZoomLevel) - { - ZoomLevel = minZoomLevel; - } - } - - private void MaxZoomLevelPropertyChanged(double maxZoomLevel) - { - if (maxZoomLevel < MinZoomLevel) - { - maxZoomLevel = MinZoomLevel; - - SetValueInternal(MaxZoomLevelProperty, maxZoomLevel); - } - - if (ZoomLevel > maxZoomLevel) - { - ZoomLevel = maxZoomLevel; - } - } - - private double CoerceZoomLevelProperty(DependencyProperty property, double zoomLevel) - { - if (zoomLevel < MinZoomLevel || zoomLevel > MaxZoomLevel) - { - zoomLevel = Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel); - - SetValueInternal(property, zoomLevel); - } - - return zoomLevel; - } - - private void ZoomLevelPropertyChanged(double zoomLevel) - { - if (!internalPropertyChange) - { - zoomLevel = CoerceZoomLevelProperty(ZoomLevelProperty, zoomLevel); - - UpdateTransform(); - - if (zoomLevelAnimation == null) - { - SetValueInternal(TargetZoomLevelProperty, zoomLevel); - } - } - } - - private void TargetZoomLevelPropertyChanged(double targetZoomLevel) - { - if (!internalPropertyChange) - { - targetZoomLevel = CoerceZoomLevelProperty(TargetZoomLevelProperty, targetZoomLevel); - - if (targetZoomLevel != ZoomLevel) - { - if (zoomLevelAnimation != null) - { - zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted; - } - - zoomLevelAnimation = new DoubleAnimation - { - To = targetZoomLevel, - Duration = AnimationDuration, - EasingFunction = AnimationEasingFunction - }; - - zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted; - - this.BeginAnimation(ZoomLevelProperty, zoomLevelAnimation); - } - } - } - - private void ZoomLevelAnimationCompleted(object sender, object e) - { - if (zoomLevelAnimation != null) - { - SetValueInternal(ZoomLevelProperty, TargetZoomLevel); - UpdateTransform(true); - - zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted; - zoomLevelAnimation = null; - - this.BeginAnimation(ZoomLevelProperty, null); - } - } - - private double CoerceHeadingProperty(DependencyProperty property, double heading) - { - if (heading < 0d || heading > 360d) - { - heading = ((heading % 360d) + 360d) % 360d; - - SetValueInternal(property, heading); - } - - return heading; - } - - private void HeadingPropertyChanged(double heading) - { - if (!internalPropertyChange) - { - heading = CoerceHeadingProperty(HeadingProperty, heading); - - UpdateTransform(); - - if (headingAnimation == null) - { - SetValueInternal(TargetHeadingProperty, heading); - } - } - } - - private void TargetHeadingPropertyChanged(double targetHeading) - { - if (!internalPropertyChange) - { - targetHeading = CoerceHeadingProperty(TargetHeadingProperty, targetHeading); - - if (targetHeading != Heading) - { - var delta = targetHeading - Heading; - - if (delta > 180d) - { - delta -= 360d; - } - else if (delta < -180d) - { - delta += 360d; - } - - if (headingAnimation != null) - { - headingAnimation.Completed -= HeadingAnimationCompleted; - } - - headingAnimation = new DoubleAnimation - { - By = delta, - Duration = AnimationDuration, - EasingFunction = AnimationEasingFunction - }; - - headingAnimation.Completed += HeadingAnimationCompleted; - - this.BeginAnimation(HeadingProperty, headingAnimation); - } - } - } - - private void HeadingAnimationCompleted(object sender, object e) - { - if (headingAnimation != null) - { - SetValueInternal(HeadingProperty, TargetHeading); - UpdateTransform(); - - headingAnimation.Completed -= HeadingAnimationCompleted; - headingAnimation = null; - - this.BeginAnimation(HeadingProperty, null); - } + ViewportChanged?.Invoke(this, e); } } } diff --git a/MapControl/Shared/MapBaseCommon.cs b/MapControl/Shared/MapBaseCommon.cs deleted file mode 100644 index cb9b7d78..00000000 --- a/MapControl/Shared/MapBaseCommon.cs +++ /dev/null @@ -1,519 +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 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/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index c5e10f6f..6c7ea0c3 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -68,9 +68,6 @@ CenteredBoundingBox.cs - - DependencyPropertyHelper.cs - EquirectangularProjection.cs @@ -107,9 +104,6 @@ MapBase.cs - - MapBaseCommon.cs - MapBorderPanel.cs @@ -230,6 +224,9 @@ Animatable.WinUI.cs + + DependencyPropertyHelper.WinUI.cs + GeoImage.WinUI.cs diff --git a/MapControl/Shared/DependencyPropertyHelper.cs b/MapControl/WPF/DependencyPropertyHelper.WPF.cs similarity index 55% rename from MapControl/Shared/DependencyPropertyHelper.cs rename to MapControl/WPF/DependencyPropertyHelper.WPF.cs index 505203dc..3aeed1a2 100644 --- a/MapControl/Shared/DependencyPropertyHelper.cs +++ b/MapControl/WPF/DependencyPropertyHelper.WPF.cs @@ -3,13 +3,7 @@ // 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 { @@ -19,26 +13,26 @@ namespace MapControl string name, TValue defaultValue = default, bool bindTwoWayByDefault = false, - Action propertyChanged = null) + Action changed = null, + Func coerce = 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) + if (changed != null) { - metadata.PropertyChangedCallback = (o, e) => propertyChanged((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue); + metadata.PropertyChangedCallback = (o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue); } -#endif + + if (coerce != null) + { + metadata.CoerceValueCallback = (o, v) => coerce((TOwner)o, (TValue)v); + } + return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata); } @@ -46,26 +40,29 @@ namespace MapControl string name, TValue defaultValue = default, bool inherits = false, - Action propertyChanged = null) + Action changed = 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) + if (changed != null) { - metadata.PropertyChangedCallback = (o, e) => propertyChanged((FrameworkElement)o, (TValue)e.OldValue, (TValue)e.NewValue); + metadata.PropertyChangedCallback = (o, e) => changed((FrameworkElement)o, (TValue)e.OldValue, (TValue)e.NewValue); } -#endif + return DependencyProperty.RegisterAttached(name, typeof(TValue), typeof(TOwner), metadata); } + + public static DependencyPropertyKey RegisterReadOnly( + string name, + TValue defaultValue = default) + where TOwner : DependencyObject + { + return DependencyProperty.RegisterReadOnly(name, typeof(TValue), typeof(TOwner), new PropertyMetadata(defaultValue)); + } } } diff --git a/MapControl/WPF/LocationAnimation.cs b/MapControl/WPF/LocationAnimation.cs new file mode 100644 index 00000000..870a3322 --- /dev/null +++ b/MapControl/WPF/LocationAnimation.cs @@ -0,0 +1,43 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// Copyright © 2024 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using System; +using System.Windows; +using System.Windows.Media.Animation; + +namespace MapControl +{ + public class LocationAnimation : AnimationTimeline + { + public override Type TargetPropertyType => typeof(Location); + + public Location To { get; set; } + + public IEasingFunction EasingFunction { get; set; } + + protected override Freezable CreateInstanceCore() + { + return new LocationAnimation + { + To = To, + EasingFunction = EasingFunction + }; + } + + public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock) + { + var from = (Location)defaultOriginValue; + var progress = animationClock.CurrentProgress ?? 1d; + + if (EasingFunction != null) + { + progress = EasingFunction.Ease(progress); + } + + return new Location( + (1d - progress) * from.Latitude + progress * To.Latitude, + (1d - progress) * from.Longitude + progress * To.Longitude); + } + } +} diff --git a/MapControl/WPF/MapBase.WPF.cs b/MapControl/WPF/MapBase.WPF.cs index 2942efca..ae6b89e3 100644 --- a/MapControl/WPF/MapBase.WPF.cs +++ b/MapControl/WPF/MapBase.WPF.cs @@ -3,53 +3,65 @@ // Licensed under the Microsoft Public License (Ms-PL) using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Animation; namespace MapControl { public partial class MapBase { - public static readonly DependencyProperty CenterProperty = DependencyProperty.Register( - nameof(Center), typeof(Location), typeof(MapBase), new FrameworkPropertyMetadata( - new Location(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - (o, e) => ((MapBase)o).CenterPropertyChanged((Location)e.NewValue))); + public static readonly DependencyProperty AnimationEasingFunctionProperty = + DependencyPropertyHelper.Register(nameof(AnimationEasingFunction), + new QuadraticEase { EasingMode = EasingMode.EaseOut }); - public static readonly DependencyProperty TargetCenterProperty = DependencyProperty.Register( - nameof(TargetCenter), typeof(Location), typeof(MapBase), new FrameworkPropertyMetadata( - new Location(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - (o, e) => ((MapBase)o).TargetCenterPropertyChanged((Location)e.NewValue))); + public static readonly DependencyProperty CenterProperty = + DependencyPropertyHelper.Register(nameof(Center), new Location(), true, + (map, oldValue, newValue) => map.CenterPropertyChanged(newValue), + (map, value) => map.CoerceCenterProperty(value)); - public static readonly DependencyProperty ZoomLevelProperty = DependencyProperty.Register( - nameof(ZoomLevel), typeof(double), typeof(MapBase), new FrameworkPropertyMetadata( - 1d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - (o, e) => ((MapBase)o).ZoomLevelPropertyChanged((double)e.NewValue))); + public static readonly DependencyProperty TargetCenterProperty = + DependencyPropertyHelper.Register(nameof(TargetCenter), new Location(), true, + (map, oldValue, newValue) => map.TargetCenterPropertyChanged(newValue), + (map, value) => map.CoerceCenterProperty(value)); - public static readonly DependencyProperty TargetZoomLevelProperty = DependencyProperty.Register( - nameof(TargetZoomLevel), typeof(double), typeof(MapBase), new FrameworkPropertyMetadata( - 1d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - (o, e) => ((MapBase)o).TargetZoomLevelPropertyChanged((double)e.NewValue))); + public static readonly DependencyProperty MinZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(MinZoomLevel), 1d, false, + (map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceMinZoomLevelProperty(value)); - public static readonly DependencyProperty HeadingProperty = DependencyProperty.Register( - nameof(Heading), typeof(double), typeof(MapBase), new FrameworkPropertyMetadata( - 0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - (o, e) => ((MapBase)o).HeadingPropertyChanged((double)e.NewValue))); + public static readonly DependencyProperty MaxZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(MaxZoomLevel), 20d, false, + (map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceMinZoomLevelProperty(value)); - public static readonly DependencyProperty TargetHeadingProperty = DependencyProperty.Register( - nameof(TargetHeading), typeof(double), typeof(MapBase), new FrameworkPropertyMetadata( - 0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - (o, e) => ((MapBase)o).TargetHeadingPropertyChanged((double)e.NewValue))); + public static readonly DependencyProperty ZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(ZoomLevel), 1d, true, + (map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceZoomLevelProperty(value)); - private static readonly DependencyPropertyKey ViewScalePropertyKey = DependencyProperty.RegisterReadOnly( - nameof(ViewScale), typeof(double), typeof(MapBase), new PropertyMetadata(0d)); + public static readonly DependencyProperty TargetZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(TargetZoomLevel), 1d, true, + (map, oldValue, newValue) => map.TargetZoomLevelPropertyChanged(newValue), + (map, value) => map.CoerceZoomLevelProperty(value)); + + public static readonly DependencyProperty HeadingProperty = + DependencyPropertyHelper.Register(nameof(Heading), 0d, true, + (map, oldValue, newValue) => map.HeadingPropertyChanged(newValue), + (map, value) => map.CoerceHeadingProperty(value)); + + public static readonly DependencyProperty TargetHeadingProperty = + DependencyPropertyHelper.Register(nameof(TargetHeading), 0d, true, + (map, oldValue, newValue) => map.TargetHeadingPropertyChanged(newValue), + (map, value) => map.CoerceHeadingProperty(value)); + + private static readonly DependencyPropertyKey ViewScalePropertyKey = + DependencyPropertyHelper.RegisterReadOnly(nameof(ViewScale), 0d); public static readonly DependencyProperty ViewScaleProperty = ViewScalePropertyKey.DependencyProperty; - private static readonly DependencyProperty CenterPointProperty = DependencyProperty.Register( - "CenterPoint", typeof(Point), typeof(MapBase), new PropertyMetadata(new Point(), - (o, e) => - { - var center = (Point)e.NewValue; - ((MapBase)o).CenterPointPropertyChanged(new Location(center.Y, center.X)); - })); + private LocationAnimation centerAnimation; + private DoubleAnimation zoomLevelAnimation; + private DoubleAnimation headingAnimation; static MapBase() { @@ -57,6 +69,36 @@ namespace MapControl DefaultStyleKeyProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(typeof(MapBase))); } + /// + /// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations. + /// The default value is a QuadraticEase with EasingMode.EaseOut. + /// + public IEasingFunction AnimationEasingFunction + { + get => (IEasingFunction)GetValue(AnimationEasingFunctionProperty); + set => SetValue(AnimationEasingFunctionProperty, value); + } + + /// + /// Gets the scaling factor from projected map coordinates to view coordinates, + /// as pixels per meter. + /// + public double ViewScale => (double)GetValue(ViewScaleProperty); + + /// + /// Gets a transform Matrix for scaling and rotating objects that are anchored + /// at a Location from map coordinates (i.e. meters) to view coordinates. + /// + public Matrix GetMapTransform(Location location) + { + var scale = GetScale(location); + + var transform = new Matrix(scale.X, 0d, 0d, scale.Y, 0d, 0d); + transform.Rotate(ViewTransform.Rotation); + + return transform; + } + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { base.OnRenderSizeChanged(sizeInfo); @@ -69,5 +111,179 @@ namespace MapControl { SetValue(ViewScalePropertyKey, scale); } + + private void CenterPropertyChanged(Location center) + { + if (!internalPropertyChange) + { + UpdateTransform(); + + if (centerAnimation == null) + { + SetValueInternal(TargetCenterProperty, center); + } + } + } + + private void TargetCenterPropertyChanged(Location targetCenter) + { + if (!internalPropertyChange && !targetCenter.Equals(Center)) + { + if (centerAnimation != null) + { + centerAnimation.Completed -= CenterAnimationCompleted; + } + + centerAnimation = new LocationAnimation + { + To = new Location(targetCenter.Latitude, ConstrainedLongitude(targetCenter.Longitude)), + Duration = AnimationDuration, + EasingFunction = AnimationEasingFunction + }; + + centerAnimation.Completed += CenterAnimationCompleted; + + BeginAnimation(CenterProperty, centerAnimation); + } + } + + private void CenterAnimationCompleted(object sender, object e) + { + if (centerAnimation != null) + { + SetValueInternal(CenterProperty, TargetCenter); + UpdateTransform(); + + centerAnimation.Completed -= CenterAnimationCompleted; + centerAnimation = null; + + BeginAnimation(CenterProperty, null); + } + } + + private void MinZoomLevelPropertyChanged(double minZoomLevel) + { + if (ZoomLevel < minZoomLevel) + { + ZoomLevel = minZoomLevel; + } + } + + private void MaxZoomLevelPropertyChanged(double maxZoomLevel) + { + if (ZoomLevel > maxZoomLevel) + { + ZoomLevel = maxZoomLevel; + } + } + + private void ZoomLevelPropertyChanged(double zoomLevel) + { + if (!internalPropertyChange) + { + UpdateTransform(); + + if (zoomLevelAnimation == null) + { + SetValueInternal(TargetZoomLevelProperty, zoomLevel); + } + } + } + + private void TargetZoomLevelPropertyChanged(double targetZoomLevel) + { + if (!internalPropertyChange && targetZoomLevel != ZoomLevel) + { + if (zoomLevelAnimation != null) + { + zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted; + } + + zoomLevelAnimation = new DoubleAnimation + { + To = targetZoomLevel, + Duration = AnimationDuration, + EasingFunction = AnimationEasingFunction + }; + + zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted; + + BeginAnimation(ZoomLevelProperty, zoomLevelAnimation); + } + } + + private void ZoomLevelAnimationCompleted(object sender, object e) + { + if (zoomLevelAnimation != null) + { + SetValueInternal(ZoomLevelProperty, TargetZoomLevel); + UpdateTransform(true); + + zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted; + zoomLevelAnimation = null; + + BeginAnimation(ZoomLevelProperty, null); + } + } + + private void HeadingPropertyChanged(double heading) + { + if (!internalPropertyChange) + { + UpdateTransform(); + + if (headingAnimation == null) + { + SetValueInternal(TargetHeadingProperty, heading); + } + } + } + + private void TargetHeadingPropertyChanged(double targetHeading) + { + if (!internalPropertyChange && targetHeading != Heading) + { + var delta = targetHeading - Heading; + + if (delta > 180d) + { + delta -= 360d; + } + else if (delta < -180d) + { + delta += 360d; + } + + if (headingAnimation != null) + { + headingAnimation.Completed -= HeadingAnimationCompleted; + } + + headingAnimation = new DoubleAnimation + { + By = delta, + Duration = AnimationDuration, + EasingFunction = AnimationEasingFunction + }; + + headingAnimation.Completed += HeadingAnimationCompleted; + + BeginAnimation(HeadingProperty, headingAnimation); + } + } + + private void HeadingAnimationCompleted(object sender, object e) + { + if (headingAnimation != null) + { + SetValueInternal(HeadingProperty, TargetHeading); + UpdateTransform(); + + headingAnimation.Completed -= HeadingAnimationCompleted; + headingAnimation = null; + + BeginAnimation(HeadingProperty, null); + } + } } } diff --git a/MapControl/WinUI/Animatable.WinUI.cs b/MapControl/WinUI/Animatable.WinUI.cs index 70dd1448..f892810e 100644 --- a/MapControl/WinUI/Animatable.WinUI.cs +++ b/MapControl/WinUI/Animatable.WinUI.cs @@ -14,41 +14,21 @@ namespace MapControl { internal static class Animatable { + public static void BeginAnimation(this DependencyObject obj, string property, Timeline animation) + { + Storyboard.SetTargetProperty(animation, property); + Storyboard.SetTarget(animation, obj); + + var storyboard = new Storyboard(); + storyboard.Children.Add(animation); + storyboard.Begin(); + } + public static void BeginAnimation(this DependencyObject obj, DependencyProperty property, Timeline animation) { - if (animation != null) + if (animation != null && property == UIElement.OpacityProperty) { - string propertyName = null; - - if (property == MapBase.CenterPointProperty) - { - propertyName = "CenterPoint"; - ((PointAnimation)animation).EnableDependentAnimation = true; - } - else if (property == MapBase.ZoomLevelProperty) - { - propertyName = "ZoomLevel"; - ((DoubleAnimation)animation).EnableDependentAnimation = true; - } - else if (property == MapBase.HeadingProperty) - { - propertyName = "Heading"; - ((DoubleAnimation)animation).EnableDependentAnimation = true; - } - else if (property == UIElement.OpacityProperty) - { - propertyName = "Opacity"; - } - - if (propertyName != null) - { - Storyboard.SetTargetProperty(animation, propertyName); - Storyboard.SetTarget(animation, obj); - - var storyboard = new Storyboard(); - storyboard.Children.Add(animation); - storyboard.Begin(); - } + BeginAnimation(obj, nameof(UIElement.Opacity), animation); } } } diff --git a/MapControl/WinUI/DependencyPropertyHelper.WinUI.cs b/MapControl/WinUI/DependencyPropertyHelper.WinUI.cs new file mode 100644 index 00000000..b14a12a9 --- /dev/null +++ b/MapControl/WinUI/DependencyPropertyHelper.WinUI.cs @@ -0,0 +1,44 @@ +// 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; +#else +using Windows.UI.Xaml; +#endif + +namespace MapControl +{ + public static class DependencyPropertyHelper + { + public static DependencyProperty Register( + string name, + TValue defaultValue = default, + bool bindTwoWayByDefault = false, + Action changed = null) + where TOwner : DependencyObject + { + var metadata = changed != null + ? new PropertyMetadata(defaultValue, (o, e) => changed((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue)) + : new PropertyMetadata(defaultValue); + + return DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), metadata); + } + + public static DependencyProperty RegisterAttached( + string name, + TValue defaultValue = default, + bool inherits = false, + Action changed = null) + where TOwner : DependencyObject + { + var metadata = changed != null + ? new PropertyMetadata(defaultValue, (o, e) => changed((FrameworkElement)o, (TValue)e.OldValue, (TValue)e.NewValue)) + : new PropertyMetadata(defaultValue); + + return DependencyProperty.RegisterAttached(name, typeof(TValue), typeof(TOwner), metadata); + } + } +} diff --git a/MapControl/WinUI/MapBase.WinUI.cs b/MapControl/WinUI/MapBase.WinUI.cs index 33dd65be..55f74f6d 100644 --- a/MapControl/WinUI/MapBase.WinUI.cs +++ b/MapControl/WinUI/MapBase.WinUI.cs @@ -6,50 +6,60 @@ using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Animation; #else using Windows.UI; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Animation; #endif namespace MapControl { public partial class MapBase { - 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))); + public static readonly DependencyProperty AnimationEasingFunctionProperty = + DependencyPropertyHelper.Register(nameof(AnimationEasingFunction), + new QuadraticEase { EasingMode = EasingMode.EaseOut }); - public static readonly DependencyProperty TargetCenterProperty = DependencyProperty.Register( - nameof(TargetCenter), typeof(Location), typeof(MapBase), - new PropertyMetadata(new Location(), (o, e) => ((MapBase)o).TargetCenterPropertyChanged((Location)e.NewValue))); + public static readonly DependencyProperty CenterProperty = + DependencyPropertyHelper.Register(nameof(Center), new Location(), true, + (map, oldValue, newValue) => map.CenterPropertyChanged(newValue)); - public static readonly DependencyProperty ZoomLevelProperty = DependencyProperty.Register( - nameof(ZoomLevel), typeof(double), typeof(MapBase), - new PropertyMetadata(1d, (o, e) => ((MapBase)o).ZoomLevelPropertyChanged((double)e.NewValue))); + public static readonly DependencyProperty TargetCenterProperty = + DependencyPropertyHelper.Register(nameof(TargetCenter), new Location(), true, + (map, oldValue, newValue) => map.TargetCenterPropertyChanged(newValue)); - public static readonly DependencyProperty TargetZoomLevelProperty = DependencyProperty.Register( - nameof(TargetZoomLevel), typeof(double), typeof(MapBase), - new PropertyMetadata(1d, (o, e) => ((MapBase)o).TargetZoomLevelPropertyChanged((double)e.NewValue))); + public static readonly DependencyProperty MinZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(MinZoomLevel), 1d, false, + (map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue)); - public static readonly DependencyProperty HeadingProperty = DependencyProperty.Register( - nameof(Heading), typeof(double), typeof(MapBase), - new PropertyMetadata(0d, (o, e) => ((MapBase)o).HeadingPropertyChanged((double)e.NewValue))); + public static readonly DependencyProperty MaxZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(MaxZoomLevel), 20d, false, + (map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue)); - public static readonly DependencyProperty TargetHeadingProperty = DependencyProperty.Register( - nameof(TargetHeading), typeof(double), typeof(MapBase), - new PropertyMetadata(0d, (o, e) => ((MapBase)o).TargetHeadingPropertyChanged((double)e.NewValue))); + public static readonly DependencyProperty ZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(ZoomLevel), 1d, true, + (map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue)); - public static readonly DependencyProperty ViewScaleProperty = DependencyProperty.Register( - nameof(ViewScale), typeof(double), typeof(MapBase), new PropertyMetadata(0d)); + public static readonly DependencyProperty TargetZoomLevelProperty = + DependencyPropertyHelper.Register(nameof(TargetZoomLevel), 1d, true, + (map, oldValue, newValue) => map.TargetZoomLevelPropertyChanged(newValue)); - internal static readonly DependencyProperty CenterPointProperty = DependencyProperty.Register( - "CenterPoint", typeof(Windows.Foundation.Point), typeof(MapBase), - new PropertyMetadata(new Windows.Foundation.Point(), (o, e) => - { - var center = (Windows.Foundation.Point)e.NewValue; - ((MapBase)o).CenterPointPropertyChanged(new Location(center.Y, center.X)); - })); + public static readonly DependencyProperty HeadingProperty = + DependencyPropertyHelper.Register(nameof(Heading), 0d, true, + (map, oldValue, newValue) => map.HeadingPropertyChanged(newValue)); + + public static readonly DependencyProperty TargetHeadingProperty = + DependencyPropertyHelper.Register(nameof(TargetHeading), 0d, true, + (map, oldValue, newValue) => map.TargetHeadingPropertyChanged(newValue)); + + public static readonly DependencyProperty ViewScaleProperty = + DependencyPropertyHelper.Register(nameof(ViewScale), 0d); + + private PointAnimation centerAnimation; + private DoubleAnimation zoomLevelAnimation; + private DoubleAnimation headingAnimation; public MapBase() { @@ -62,6 +72,36 @@ namespace MapControl SizeChanged += OnSizeChanged; } + /// + /// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations. + /// The default value is a QuadraticEase with EasingMode.EaseOut. + /// + public EasingFunctionBase AnimationEasingFunction + { + get => (EasingFunctionBase)GetValue(AnimationEasingFunctionProperty); + set => SetValue(AnimationEasingFunctionProperty, value); + } + + /// + /// Gets the scaling factor from projected map coordinates to view coordinates, + /// as pixels per meter. + /// + public double ViewScale => (double)GetValue(ViewScaleProperty); + + /// + /// Gets a transform Matrix for scaling and rotating objects that are anchored + /// at a Location from map coordinates (i.e. meters) to view coordinates. + /// + public Matrix GetMapTransform(Location location) + { + var scale = GetScale(location); + + var transform = new Matrix(scale.X, 0d, 0d, scale.Y, 0d, 0d); + transform.Rotate(ViewTransform.Rotation); + + return transform; + } + private void OnSizeChanged(object sender, SizeChangedEventArgs e) { Clip = new RectangleGeometry @@ -77,5 +117,248 @@ namespace MapControl { SetValue(ViewScaleProperty, scale); } + + private void CenterPropertyChanged(Location value) + { + if (!internalPropertyChange) + { + var center = CoerceCenterProperty(value); + + if (!center.Equals(value)) + { + SetValueInternal(CenterProperty, center); + } + + UpdateTransform(); + + if (centerAnimation == null) + { + SetValueInternal(TargetCenterProperty, center); + } + } + } + +#pragma warning disable IDE0052 // Remove unread private members + private static readonly DependencyProperty CenterPointProperty = + DependencyPropertyHelper.Register("CenterPoint", + new Windows.Foundation.Point(), false, (map, oldValue, newValue) => map.Center = new Location(newValue.Y, newValue.X)); +#pragma warning restore IDE0052 + + private void TargetCenterPropertyChanged(Location value) + { + if (!internalPropertyChange) + { + var targetCenter = CoerceCenterProperty(value); + + if (!targetCenter.Equals(value)) + { + SetValueInternal(TargetCenterProperty, targetCenter); + } + + if (!targetCenter.Equals(Center)) + { + if (centerAnimation != null) + { + centerAnimation.Completed -= CenterAnimationCompleted; + } + + centerAnimation = new PointAnimation + { + From = new Windows.Foundation.Point(Center.Longitude, Center.Latitude), + To = new Windows.Foundation.Point(ConstrainedLongitude(targetCenter.Longitude), targetCenter.Latitude), + Duration = AnimationDuration, + EasingFunction = AnimationEasingFunction, + EnableDependentAnimation = true + }; + + centerAnimation.Completed += CenterAnimationCompleted; + + this.BeginAnimation("CenterPoint", centerAnimation); + } + } + } + + private void CenterAnimationCompleted(object sender, object e) + { + if (centerAnimation != null) + { + SetValueInternal(CenterProperty, TargetCenter); + UpdateTransform(); + + centerAnimation.Completed -= CenterAnimationCompleted; + centerAnimation = null; + } + } + + private void MinZoomLevelPropertyChanged(double value) + { + var minZoomLevel = CoerceMinZoomLevelProperty(value); + + if (minZoomLevel != value) + { + SetValueInternal(MinZoomLevelProperty, minZoomLevel); + } + + if (ZoomLevel < minZoomLevel) + { + ZoomLevel = minZoomLevel; + } + } + + private void MaxZoomLevelPropertyChanged(double value) + { + var maxZoomLevel = CoerceMaxZoomLevelProperty(value); + + if (maxZoomLevel != value) + { + SetValueInternal(MaxZoomLevelProperty, maxZoomLevel); + } + + if (ZoomLevel > maxZoomLevel) + { + ZoomLevel = maxZoomLevel; + } + } + + private void ZoomLevelPropertyChanged(double value) + { + if (!internalPropertyChange) + { + var zoomLevel = CoerceZoomLevelProperty(value); + + if (zoomLevel != value) + { + SetValueInternal(ZoomLevelProperty, zoomLevel); + } + + UpdateTransform(); + + if (zoomLevelAnimation == null) + { + SetValueInternal(TargetZoomLevelProperty, zoomLevel); + } + } + } + + private void TargetZoomLevelPropertyChanged(double value) + { + if (!internalPropertyChange) + { + var targetZoomLevel = CoerceZoomLevelProperty(value); + + if (targetZoomLevel != value) + { + SetValueInternal(TargetZoomLevelProperty, targetZoomLevel); + } + + if (targetZoomLevel != ZoomLevel) + { + if (zoomLevelAnimation != null) + { + zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted; + } + + zoomLevelAnimation = new DoubleAnimation + { + To = targetZoomLevel, + Duration = AnimationDuration, + EasingFunction = AnimationEasingFunction, + EnableDependentAnimation = true + }; + + zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted; + + this.BeginAnimation(nameof(ZoomLevel), zoomLevelAnimation); + } + } + } + + private void ZoomLevelAnimationCompleted(object sender, object e) + { + if (zoomLevelAnimation != null) + { + SetValueInternal(ZoomLevelProperty, TargetZoomLevel); + UpdateTransform(true); + + zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted; + zoomLevelAnimation = null; + } + } + + private void HeadingPropertyChanged(double value) + { + if (!internalPropertyChange) + { + var heading = CoerceHeadingProperty(value); + + if (heading != value) + { + SetValueInternal(HeadingProperty, heading); + } + + UpdateTransform(); + + if (headingAnimation == null) + { + SetValueInternal(TargetHeadingProperty, heading); + } + } + } + + private void TargetHeadingPropertyChanged(double value) + { + if (!internalPropertyChange) + { + var targetHeading = CoerceHeadingProperty(value); + + if (targetHeading != value) + { + SetValueInternal(TargetHeadingProperty, targetHeading); + } + + if (targetHeading != Heading) + { + var delta = targetHeading - Heading; + + if (delta > 180d) + { + delta -= 360d; + } + else if (delta < -180d) + { + delta += 360d; + } + + if (headingAnimation != null) + { + headingAnimation.Completed -= HeadingAnimationCompleted; + } + + headingAnimation = new DoubleAnimation + { + By = delta, + Duration = AnimationDuration, + EasingFunction = AnimationEasingFunction, + EnableDependentAnimation = true + }; + + headingAnimation.Completed += HeadingAnimationCompleted; + + this.BeginAnimation(nameof(Heading), headingAnimation); + } + } + } + + private void HeadingAnimationCompleted(object sender, object e) + { + if (headingAnimation != null) + { + SetValueInternal(HeadingProperty, TargetHeading); + UpdateTransform(); + + headingAnimation.Completed -= HeadingAnimationCompleted; + headingAnimation = null; + } + } } }