// 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; using Microsoft.UI.Xaml.Media.Animation; #elif UWP using Windows.UI.Xaml; using Windows.UI.Xaml.Media.Animation; #else using System.Windows; using System.Windows.Media; using System.Windows.Media.Animation; #endif namespace MapControl { public partial class MapBase { public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register( nameof(MinZoomLevel), typeof(double), typeof(MapBase), new PropertyMetadata(1d, (o, e) => ((MapBase)o).MinZoomLevelPropertyChanged((double)e.NewValue))); public static readonly DependencyProperty MaxZoomLevelProperty = DependencyProperty.Register( nameof(MaxZoomLevel), typeof(double), typeof(MapBase), new PropertyMetadata(20d, (o, e) => ((MapBase)o).MaxZoomLevelPropertyChanged((double)e.NewValue))); public static readonly DependencyProperty AnimationEasingFunctionProperty = DependencyProperty.Register( nameof(AnimationEasingFunction), typeof(EasingFunctionBase), typeof(MapBase), new PropertyMetadata(new QuadraticEase { EasingMode = EasingMode.EaseOut })); private PointAnimation centerAnimation; private DoubleAnimation zoomLevelAnimation; private DoubleAnimation headingAnimation; /// /// 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 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; CoerceCenterProperty(CenterProperty, Center); } } ResetTransformCenter(); UpdateTransform(false, true); } private Location CoerceCenterProperty(DependencyProperty property, Location center) { 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; } private void CenterPropertyChanged(Location center) { if (!internalPropertyChange) { center = CoerceCenterProperty(CenterProperty, center); UpdateTransform(); if (centerAnimation == null) { SetValueInternal(TargetCenterProperty, center); } } } private void TargetCenterPropertyChanged(Location targetCenter) { if (!internalPropertyChange) { targetCenter = CoerceCenterProperty(TargetCenterProperty, targetCenter); if (!targetCenter.Equals(Center)) { if (centerAnimation != null) { centerAnimation.Completed -= CenterAnimationCompleted; } 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); } } } private void CenterAnimationCompleted(object sender, object e) { if (centerAnimation != null) { centerAnimation.Completed -= CenterAnimationCompleted; centerAnimation = null; 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); } } } }