XAML-Map-Control/MapControl/Avalonia/MapBase.Avalonia.cs

287 lines
10 KiB
C#
Raw Normal View History

2024-05-19 23:23:27 +02:00
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// Copyright © 2024 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
global using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Styling;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MapControl
{
2024-05-20 23:24:34 +02:00
public partial class MapBase
2024-05-19 23:23:27 +02:00
{
public static readonly StyledProperty<Location> CenterProperty
= AvaloniaProperty.Register<MapBase, Location>(nameof(Center), new Location(), false,
BindingMode.TwoWay, null, (map, center) => ((MapBase)map).CoerceCenterProperty(center));
2024-05-20 23:24:34 +02:00
public static readonly StyledProperty<Location> TargetCenterProperty
= AvaloniaProperty.Register<MapBase, Location>(nameof(TargetCenter), new Location(), false,
BindingMode.TwoWay, null, (map, center) => ((MapBase)map).CoerceCenterProperty(center));
2024-05-19 23:23:27 +02:00
public static readonly StyledProperty<double> MinZoomLevelProperty
= AvaloniaProperty.Register<MapBase, double>(nameof(MinZoomLevel), 1d, false,
BindingMode.OneWay, null, (map, minZoomLevel) => ((MapBase)map).CoerceMinZoomLevelProperty(minZoomLevel));
public static readonly StyledProperty<double> MaxZoomLevelProperty
= AvaloniaProperty.Register<MapBase, double>(nameof(MaxZoomLevel), 20d, false,
BindingMode.OneWay, null, (map, maxZoomLevel) => ((MapBase)map).CoerceMaxZoomLevelProperty(maxZoomLevel));
public static readonly StyledProperty<double> ZoomLevelProperty
= AvaloniaProperty.Register<MapBase, double>(nameof(ZoomLevel), 1d, false,
BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel));
2024-05-20 23:24:34 +02:00
public static readonly StyledProperty<double> TargetZoomLevelProperty
= AvaloniaProperty.Register<MapBase, double>(nameof(TargetZoomLevel), 1d, false,
BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel));
2024-05-19 23:23:27 +02:00
public static readonly StyledProperty<double> HeadingProperty
= AvaloniaProperty.Register<MapBase, double>(nameof(Heading), 0d, false,
2024-05-20 23:24:34 +02:00
BindingMode.TwoWay, null, (map, heading) => CoerceHeadingProperty(heading));
public static readonly StyledProperty<double> TargetHeadingProperty
= AvaloniaProperty.Register<MapBase, double>(nameof(TargetHeading), 0d, false,
BindingMode.TwoWay, null, (map, heading) => CoerceHeadingProperty(heading));
2024-05-19 23:23:27 +02:00
public static readonly DirectProperty<MapBase, double> ViewScaleProperty
= AvaloniaProperty.RegisterDirect<MapBase, double>(nameof(ViewScale), map => map.ViewScale);
2024-05-20 23:24:34 +02:00
private CancellationTokenSource centerCts;
private CancellationTokenSource zoomLevelCts;
private CancellationTokenSource headingCts;
private Animation centerAnimation;
private Animation zoomLevelAnimation;
private Animation headingAnimation;
2024-05-19 23:23:27 +02:00
static MapBase()
{
2024-05-20 23:24:34 +02:00
ClipToBoundsProperty.OverrideDefaultValue(typeof(MapBase), true);
2024-05-19 23:23:27 +02:00
CenterProperty.Changed.AddClassHandler<MapBase, Location>(
2024-05-20 23:24:34 +02:00
(map, args) => map.CenterPropertyChanged(args.NewValue.Value));
2024-05-19 23:23:27 +02:00
ZoomLevelProperty.Changed.AddClassHandler<MapBase, double>(
2024-05-20 23:24:34 +02:00
(map, args) => map.ZoomLevelPropertyChanged(args.NewValue.Value));
TargetZoomLevelProperty.Changed.AddClassHandler<MapBase, double>(
async (map, args) => await map.TargetZoomLevelPropertyChanged(args.NewValue.Value));
2024-05-19 23:23:27 +02:00
HeadingProperty.Changed.AddClassHandler<MapBase, double>(
2024-05-20 23:24:34 +02:00
(map, args) => map.HeadingPropertyChanged(args.NewValue.Value));
TargetHeadingProperty.Changed.AddClassHandler<MapBase, double>(
async (map, args) => await map.TargetHeadingPropertyChanged(args.NewValue.Value));
2024-05-19 23:23:27 +02:00
}
public MapBase()
{
MapProjectionPropertyChanged(MapProjection);
}
2024-05-20 23:24:34 +02:00
internal Size RenderSize => Bounds.Size;
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
protected override void OnSizeChanged(SizeChangedEventArgs e)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
base.OnSizeChanged(e);
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
ResetTransformCenter();
UpdateTransform();
2024-05-19 23:23:27 +02:00
}
/// <summary>
/// Gets the scaling factor from projected map coordinates to view coordinates,
/// as pixels per meter.
/// </summary>
public double ViewScale
{
get => ViewTransform.Scale;
}
2024-05-20 23:24:34 +02:00
private void SetViewScale(double viewScale)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
RaisePropertyChanged(ViewScaleProperty, double.NaN, viewScale);
2024-05-19 23:23:27 +02:00
}
/// <summary>
/// Gets a transform Matrix for scaling and rotating objects that are anchored
/// at a Location from map coordinates (i.e. meters) to view coordinates.
/// </summary>
public Matrix GetMapTransform(Location location)
{
var scale = GetScale(location);
return new Matrix(scale.X, 0d, 0d, scale.Y, 0d, 0d)
.Append(Matrix.CreateRotation(ViewTransform.Rotation));
}
2024-05-20 23:24:34 +02:00
private void MapProjectionPropertyChanged(MapProjection projection)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
maxLatitude = 90d;
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
if (projection.Type <= MapProjectionType.NormalCylindrical)
{
var maxLocation = projection.MapToLocation(new Point(0d, 180d * MapProjection.Wgs84MeterPerDegree));
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
if (maxLocation != null && maxLocation.Latitude < 90d)
{
maxLatitude = maxLocation.Latitude;
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
Center = CoerceCenterProperty(Center);
}
}
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
ResetTransformCenter();
UpdateTransform(false, true);
2024-05-19 23:23:27 +02:00
}
2024-05-20 23:24:34 +02:00
private Location CoerceCenterProperty(Location center)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
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));
}
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
return center;
2024-05-19 23:23:27 +02:00
}
2024-05-20 23:24:34 +02:00
private void CenterPropertyChanged(Location center)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
if (!internalPropertyChange)
2024-05-19 23:23:27 +02:00
{
UpdateTransform();
}
2024-05-20 23:24:34 +02:00
}
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
private double CoerceMinZoomLevelProperty(double minZoomLevel)
{
return Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel);
2024-05-19 23:23:27 +02:00
}
2024-05-20 23:24:34 +02:00
private double CoerceMaxZoomLevelProperty(double maxZoomLevel)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
return Math.Max(maxZoomLevel, MinZoomLevel);
}
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
private double CoerceZoomLevelProperty(double zoomLevel)
{
return Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
}
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
private void ZoomLevelPropertyChanged(double zoomLevel)
{
if (!internalPropertyChange)
{
UpdateTransform();
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
if (zoomLevelAnimation == null)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
2024-05-19 23:23:27 +02:00
}
}
}
2024-05-20 23:24:34 +02:00
private async Task TargetZoomLevelPropertyChanged(double targetZoomLevel)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
if (!internalPropertyChange && targetZoomLevel != ZoomLevel)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
zoomLevelCts?.Cancel();
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
zoomLevelAnimation = new Animation
2024-05-19 23:23:27 +02:00
{
FillMode = FillMode.Forward,
Duration = AnimationDuration,
Children =
{
new KeyFrame
{
KeyTime = AnimationDuration,
2024-05-20 23:24:34 +02:00
Setters = { new Setter(ZoomLevelProperty, targetZoomLevel) }
2024-05-19 23:23:27 +02:00
}
}
};
2024-05-20 23:24:34 +02:00
zoomLevelCts = new CancellationTokenSource();
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token);
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
zoomLevelCts.Dispose();
zoomLevelCts = null;
zoomLevelAnimation = null;
}
2024-05-19 23:23:27 +02:00
}
2024-05-20 23:24:34 +02:00
private static double CoerceHeadingProperty(double heading)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
return ((heading % 360d) + 360d) % 360d;
2024-05-19 23:23:27 +02:00
}
2024-05-20 23:24:34 +02:00
private void HeadingPropertyChanged(double heading)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
if (!internalPropertyChange)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
UpdateTransform();
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
if (headingAnimation == null)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
SetValueInternal(TargetHeadingProperty, heading);
2024-05-19 23:23:27 +02:00
}
}
}
2024-05-20 23:24:34 +02:00
private async Task TargetHeadingPropertyChanged(double targetHeading)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
if (!internalPropertyChange && targetHeading != Heading)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
var delta = targetHeading - Heading;
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
if (delta > 180d)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
delta -= 360d;
2024-05-19 23:23:27 +02:00
}
2024-05-20 23:24:34 +02:00
else if (delta < -180d)
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
delta += 360d;
2024-05-19 23:23:27 +02:00
}
2024-05-20 23:24:34 +02:00
targetHeading = Heading + delta;
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
headingCts?.Cancel();
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
headingAnimation = new Animation
2024-05-19 23:23:27 +02:00
{
2024-05-20 23:24:34 +02:00
FillMode = FillMode.Forward,
Duration = AnimationDuration,
Children =
{
new KeyFrame
{
KeyTime = AnimationDuration,
Setters = { new Setter(HeadingProperty, targetHeading) }
}
}
};
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
headingCts = new CancellationTokenSource();
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
await headingAnimation.RunAsync(this, headingCts.Token);
2024-05-19 23:23:27 +02:00
2024-05-20 23:24:34 +02:00
headingCts.Dispose();
headingCts = null;
headingAnimation = null;
2024-05-19 23:23:27 +02:00
}
}
}
}