using Avalonia; using Avalonia.Animation; using Avalonia.Animation.Easings; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Data; using Avalonia.Media; using Avalonia.Styling; using System.Threading; using System.Threading.Tasks; using Brush = Avalonia.Media.IBrush; namespace MapControl { public partial class MapBase { public static readonly StyledProperty ForegroundProperty = TextElement.ForegroundProperty.AddOwner(new StyledPropertyMetadata(new Optional(Brushes.Black))); public static readonly StyledProperty AnimationEasingProperty = DependencyPropertyHelper.Register(nameof(AnimationEasing), new QuadraticEaseOut()); public static readonly StyledProperty CenterProperty = DependencyPropertyHelper.Register(nameof(Center), new Location(), (map, oldValue, newValue) => map.CenterPropertyChanged(newValue), (map, value) => map.CoerceCenterProperty(value), true); public static readonly StyledProperty TargetCenterProperty = DependencyPropertyHelper.Register(nameof(TargetCenter), new Location(), async (map, oldValue, newValue) => await map.TargetCenterPropertyChanged(newValue), (map, value) => map.CoerceCenterProperty(value), true); public static readonly StyledProperty MinZoomLevelProperty = DependencyPropertyHelper.Register(nameof(MinZoomLevel), 1d, (map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue), (map, value) => map.CoerceMinZoomLevelProperty(value)); public static readonly StyledProperty MaxZoomLevelProperty = DependencyPropertyHelper.Register(nameof(MaxZoomLevel), 20d, (map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue), (map, value) => map.CoerceMaxZoomLevelProperty(value)); public static readonly StyledProperty ZoomLevelProperty = DependencyPropertyHelper.Register(nameof(ZoomLevel), 1d, (map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue), (map, value) => map.CoerceZoomLevelProperty(value), true); public static readonly StyledProperty TargetZoomLevelProperty = DependencyPropertyHelper.Register(nameof(TargetZoomLevel), 1d, async (map, oldValue, newValue) => await map.TargetZoomLevelPropertyChanged(newValue), (map, value) => map.CoerceZoomLevelProperty(value), true); public static readonly StyledProperty HeadingProperty = DependencyPropertyHelper.Register(nameof(Heading), 0d, (map, oldValue, newValue) => map.HeadingPropertyChanged(newValue), (map, value) => map.CoerceHeadingProperty(value), true); public static readonly StyledProperty TargetHeadingProperty = DependencyPropertyHelper.Register(nameof(TargetHeading), 0d, async (map, oldValue, newValue) => await map.TargetHeadingPropertyChanged(newValue), (map, value) => map.CoerceHeadingProperty(value), true); public static readonly DirectProperty ViewScaleProperty = AvaloniaProperty.RegisterDirect(nameof(ViewScale), map => map.ViewTransform.Scale); private CancellationTokenSource centerCts; private CancellationTokenSource zoomLevelCts; private CancellationTokenSource headingCts; private Animation centerAnimation; private Animation zoomLevelAnimation; private Animation headingAnimation; static MapBase() { BackgroundProperty.OverrideDefaultValue(typeof(MapBase), Brushes.White); ClipToBoundsProperty.OverrideDefaultValue(typeof(MapBase), true); Animation.RegisterCustomAnimator(); } public double ActualWidth => Bounds.Width; public double ActualHeight => Bounds.Height; protected override void OnSizeChanged(SizeChangedEventArgs e) { base.OnSizeChanged(e); ResetTransformCenter(); UpdateTransform(); } /// /// Gets or sets the Easing of the Center, ZoomLevel and Heading animations. /// The default value is a QuadraticEaseOut. /// public Easing AnimationEasing { get => GetValue(AnimationEasingProperty); set => SetValue(AnimationEasingProperty, value); } /// /// Gets the scaling factor from projected map coordinates to view coordinates, /// as pixels per meter. /// public double ViewScale { get => GetValue(ViewScaleProperty); private set => RaisePropertyChanged(ViewScaleProperty, double.NaN, value); } private void CenterPropertyChanged(Location center) { if (!internalPropertyChange) { UpdateTransform(); if (centerAnimation == null) { SetValueInternal(TargetCenterProperty, center); } } } private async Task TargetCenterPropertyChanged(Location targetCenter) { if (!internalPropertyChange && !targetCenter.Equals(Center)) { ResetTransformCenter(); centerCts?.Cancel(); centerAnimation = CreateAnimation(CenterProperty, new Location(targetCenter.Latitude, CoerceLongitude(targetCenter.Longitude))); using (centerCts = new CancellationTokenSource()) { await centerAnimation.RunAsync(this, centerCts.Token); if (!centerCts.IsCancellationRequested) { UpdateTransform(); } } centerCts = null; centerAnimation = 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 async Task TargetZoomLevelPropertyChanged(double targetZoomLevel) { if (!internalPropertyChange && targetZoomLevel != ZoomLevel) { zoomLevelCts?.Cancel(); zoomLevelAnimation = CreateAnimation(ZoomLevelProperty, targetZoomLevel); using (zoomLevelCts = new CancellationTokenSource()) { await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token); if (!zoomLevelCts.IsCancellationRequested) { UpdateTransform(true); // reset transform center } } zoomLevelCts = null; zoomLevelAnimation = null; } } 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 = CreateAnimation(HeadingProperty, targetHeading); using (headingCts = new CancellationTokenSource()) { await headingAnimation.RunAsync(this, headingCts.Token); if (!headingCts.IsCancellationRequested) { UpdateTransform(); } } headingCts = null; headingAnimation = null; } } private Animation CreateAnimation(DependencyProperty property, object value) { return new Animation { FillMode = FillMode.Forward, Duration = AnimationDuration, Easing = AnimationEasing, Children = { new KeyFrame { KeyTime = AnimationDuration, Setters = { new Setter(property, value) } } } }; } } }