mirror of
https://github.com/ClemensFischer/XAML-Map-Control.git
synced 2026-02-11 02:04:15 +01:00
MapBase dependency properties
This commit is contained in:
parent
abe3bb75f9
commit
74f4e0176b
|
|
@ -7,41 +7,49 @@ namespace MapControl
|
|||
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001")]
|
||||
public static class DependencyPropertyHelper
|
||||
{
|
||||
public static AvaloniaProperty Register<TOwner, TValue>(
|
||||
public static StyledProperty<TValue> Register<TOwner, TValue>(
|
||||
string name,
|
||||
TValue defaultValue = default,
|
||||
bool bindTwoWayByDefault = false,
|
||||
Action<TOwner, TValue, TValue> propertyChanged = null)
|
||||
Action<TOwner, TValue, TValue> changed = null,
|
||||
Func<TOwner, TValue, TValue> coerce = null)
|
||||
where TOwner : AvaloniaObject
|
||||
{
|
||||
StyledProperty<TValue> property = AvaloniaProperty.Register<TOwner, TValue>(name, defaultValue, false,
|
||||
bindTwoWayByDefault ? Avalonia.Data.BindingMode.TwoWay : Avalonia.Data.BindingMode.OneWay);
|
||||
var property = AvaloniaProperty.Register<TOwner, TValue>(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<TOwner, TValue>(
|
||||
(o, e) => propertyChanged(o, e.OldValue.Value, e.NewValue.Value));
|
||||
property.Changed.AddClassHandler<TOwner, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
|
||||
}
|
||||
|
||||
return property;
|
||||
}
|
||||
|
||||
public static AvaloniaProperty RegisterAttached<TOwner, TValue>(
|
||||
public static AttachedProperty<TValue> RegisterAttached<TOwner, TValue>(
|
||||
string name,
|
||||
TValue defaultValue = default,
|
||||
bool inherits = false,
|
||||
Action<Control, TValue, TValue> propertyChanged = null)
|
||||
Action<Control, TValue, TValue> changed = null)
|
||||
where TOwner : AvaloniaObject
|
||||
{
|
||||
AttachedProperty<TValue> property = AvaloniaProperty.RegisterAttached<TOwner, Control, TValue>(name, defaultValue, inherits);
|
||||
var property = AvaloniaProperty.RegisterAttached<TOwner, Control, TValue>(name, defaultValue, inherits);
|
||||
|
||||
if (propertyChanged != null)
|
||||
if (changed != null)
|
||||
{
|
||||
property.Changed.AddClassHandler<Control, TValue>(
|
||||
(o, e) => propertyChanged(o, e.OldValue.Value, e.NewValue.Value));
|
||||
property.Changed.AddClassHandler<Control, TValue>((o, e) => changed(o, e.OldValue.Value, e.NewValue.Value));
|
||||
}
|
||||
|
||||
return property;
|
||||
}
|
||||
|
||||
public static DirectProperty<TOwner, TValue> RegisterReadOnly<TOwner, TValue>(
|
||||
string name,
|
||||
Func<TOwner, TValue> getter)
|
||||
where TOwner : AvaloniaObject
|
||||
{
|
||||
return AvaloniaProperty.RegisterDirect(name, getter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ namespace MapControl
|
|||
/// </summary>
|
||||
public class Map : MapBase
|
||||
{
|
||||
public static readonly StyledProperty<double> MouseWheelZoomDeltaProperty
|
||||
= AvaloniaProperty.Register<Map, double>(nameof(MouseWheelZoomDelta), 0.25);
|
||||
public static readonly StyledProperty<double> MouseWheelZoomDeltaProperty =
|
||||
DependencyPropertyHelper.Register<Map, double>(nameof(MouseWheelZoomDelta), 0.25);
|
||||
|
||||
private Point? mousePosition;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Location> CenterProperty
|
||||
= AvaloniaProperty.Register<MapBase, Location>(nameof(Center), new Location(), false,
|
||||
BindingMode.TwoWay, null, (map, center) => ((MapBase)map).CoerceCenterProperty(center));
|
||||
public static readonly StyledProperty<Location> CenterProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, Location>(nameof(Center), new Location(), true,
|
||||
(map, oldValue, newValue) => map.CenterPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceCenterProperty(value));
|
||||
|
||||
public static readonly StyledProperty<Location> TargetCenterProperty
|
||||
= AvaloniaProperty.Register<MapBase, Location>(nameof(TargetCenter), new Location(), false,
|
||||
BindingMode.TwoWay, null, (map, center) => ((MapBase)map).CoerceCenterProperty(center));
|
||||
public static readonly StyledProperty<Location> TargetCenterProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, Location>(nameof(TargetCenter), new Location(), true,
|
||||
async (map, oldValue, newValue) => await map.TargetCenterPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceCenterProperty(value));
|
||||
|
||||
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> MinZoomLevelProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(MinZoomLevel), 1d, false,
|
||||
(map, oldValue, newValue) => map.MinZoomLevelPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceMinZoomLevelProperty(value));
|
||||
|
||||
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> MaxZoomLevelProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(MaxZoomLevel), 20d, false,
|
||||
(map, oldValue, newValue) => map.MaxZoomLevelPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceMinZoomLevelProperty(value));
|
||||
|
||||
public static readonly StyledProperty<double> ZoomLevelProperty
|
||||
= AvaloniaProperty.Register<MapBase, double>(nameof(ZoomLevel), 1d, false,
|
||||
BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel));
|
||||
public static readonly StyledProperty<double> ZoomLevelProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(ZoomLevel), 1d, true,
|
||||
(map, oldValue, newValue) => map.ZoomLevelPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceZoomLevelProperty(value));
|
||||
|
||||
public static readonly StyledProperty<double> TargetZoomLevelProperty
|
||||
= AvaloniaProperty.Register<MapBase, double>(nameof(TargetZoomLevel), 1d, false,
|
||||
BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel));
|
||||
public static readonly StyledProperty<double> TargetZoomLevelProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetZoomLevel), 1d, true,
|
||||
async (map, oldValue, newValue) => await map.TargetZoomLevelPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceZoomLevelProperty(value));
|
||||
|
||||
public static readonly StyledProperty<double> HeadingProperty
|
||||
= AvaloniaProperty.Register<MapBase, double>(nameof(Heading), 0d, false,
|
||||
BindingMode.TwoWay, null, (map, heading) => CoerceHeadingProperty(heading));
|
||||
public static readonly StyledProperty<double> HeadingProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d, true,
|
||||
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceHeadingProperty(value));
|
||||
|
||||
public static readonly StyledProperty<double> TargetHeadingProperty
|
||||
= AvaloniaProperty.Register<MapBase, double>(nameof(TargetHeading), 0d, false,
|
||||
BindingMode.TwoWay, null, (map, heading) => CoerceHeadingProperty(heading));
|
||||
public static readonly StyledProperty<double> TargetHeadingProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d, true,
|
||||
async (map, oldValue, newValue) => await map.TargetHeadingPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceHeadingProperty(value));
|
||||
|
||||
public static readonly DirectProperty<MapBase, double> ViewScaleProperty
|
||||
= AvaloniaProperty.RegisterDirect<MapBase, double>(nameof(ViewScale), map => map.ViewScale);
|
||||
public static readonly DirectProperty<MapBase, double> ViewScaleProperty =
|
||||
DependencyPropertyHelper.RegisterReadOnly<MapBase, double>(nameof(ViewScale), map => map.ViewScale);
|
||||
|
||||
private CancellationTokenSource centerCts;
|
||||
private CancellationTokenSource zoomLevelCts;
|
||||
|
|
@ -59,27 +65,9 @@ namespace MapControl
|
|||
|
||||
static MapBase()
|
||||
{
|
||||
Animation.RegisterCustomAnimator<Location, LocationAnimator>();
|
||||
|
||||
ClipToBoundsProperty.OverrideDefaultValue(typeof(MapBase), true);
|
||||
|
||||
CenterProperty.Changed.AddClassHandler<MapBase, Location>(
|
||||
(map, args) => map.CenterPropertyChanged(args.NewValue.Value));
|
||||
|
||||
TargetCenterProperty.Changed.AddClassHandler<MapBase, Location>(
|
||||
async (map, args) => await map.TargetCenterPropertyChanged(args.NewValue.Value));
|
||||
|
||||
ZoomLevelProperty.Changed.AddClassHandler<MapBase, double>(
|
||||
(map, args) => map.ZoomLevelPropertyChanged(args.NewValue.Value));
|
||||
|
||||
TargetZoomLevelProperty.Changed.AddClassHandler<MapBase, double>(
|
||||
async (map, args) => await map.TargetZoomLevelPropertyChanged(args.NewValue.Value));
|
||||
|
||||
HeadingProperty.Changed.AddClassHandler<MapBase, double>(
|
||||
(map, args) => map.HeadingPropertyChanged(args.NewValue.Value));
|
||||
|
||||
TargetHeadingProperty.Changed.AddClassHandler<MapBase, double>(
|
||||
async (map, args) => await map.TargetHeadingPropertyChanged(args.NewValue.Value));
|
||||
Animation.RegisterCustomAnimator<Location, LocationAnimator>();
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<Compile Include="..\Shared\ImageLoader.cs" Link="ImageLoader.cs" />
|
||||
<Compile Include="..\Shared\Location.cs" Link="Location.cs" />
|
||||
<Compile Include="..\Shared\LocationCollection.cs" Link="LocationCollection.cs" />
|
||||
<Compile Include="..\Shared\MapBaseCommon.cs" Link="MapBaseCommon.cs" />
|
||||
<Compile Include="..\Shared\MapBase.cs" Link="MapBase.cs" />
|
||||
<Compile Include="..\Shared\MapPanel.cs" Link="MapPanel.cs" />
|
||||
<Compile Include="..\Shared\MapProjection.cs" Link="MapProjection.cs" />
|
||||
<Compile Include="..\Shared\MapTileLayer.cs" Link="MapTileLayer.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)));
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<MapBase, Brush>(nameof(Foreground), new SolidColorBrush(Colors.Black));
|
||||
|
||||
private PointAnimation centerAnimation;
|
||||
private DoubleAnimation zoomLevelAnimation;
|
||||
private DoubleAnimation headingAnimation;
|
||||
public static readonly DependencyProperty AnimationDurationProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, TimeSpan>(nameof(AnimationDuration), TimeSpan.FromSeconds(0.3));
|
||||
|
||||
public static readonly DependencyProperty MapLayerProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, UIElement>(nameof(MapLayer), null, false,
|
||||
(map, oldValue, newValue) => map.MapLayerPropertyChanged(oldValue, newValue));
|
||||
|
||||
public static readonly DependencyProperty MapProjectionProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, MapProjection>(nameof(MapProjection), new WebMercatorProjection(), false,
|
||||
(map, oldValue, newValue) => map.MapProjectionPropertyChanged(newValue));
|
||||
|
||||
public static readonly DependencyProperty ProjectionCenterProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, Location>(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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public EasingFunctionBase AnimationEasingFunction
|
||||
public event EventHandler<ViewportChangedEventArgs> ViewportChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the map foreground Brush.
|
||||
/// </summary>
|
||||
public Brush Foreground
|
||||
{
|
||||
get => (EasingFunctionBase)GetValue(AnimationEasingFunctionProperty);
|
||||
set => SetValue(AnimationEasingFunctionProperty, value);
|
||||
get => (Brush)GetValue(ForegroundProperty);
|
||||
set => SetValue(ForegroundProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public double ViewScale => (double)GetValue(ViewScaleProperty);
|
||||
public TimeSpan AnimationDuration
|
||||
{
|
||||
get => (TimeSpan)GetValue(AnimationDurationProperty);
|
||||
set => SetValue(AnimationDurationProperty, value);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
/// <summary>
|
||||
/// Gets or sets the MapProjection used by the map control.
|
||||
/// </summary>
|
||||
public MapProjection MapProjection
|
||||
{
|
||||
get => (MapProjection)GetValue(MapProjectionProperty);
|
||||
set => SetValue(MapProjectionProperty, value);
|
||||
}
|
||||
|
||||
return transform;
|
||||
/// <summary>
|
||||
/// Gets or sets an optional center (reference point) for azimuthal projections.
|
||||
/// If ProjectionCenter is null, the Center property value will be used instead.
|
||||
/// </summary>
|
||||
public Location ProjectionCenter
|
||||
{
|
||||
get => (Location)GetValue(ProjectionCenterProperty);
|
||||
set => SetValue(ProjectionCenterProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location of the center point of the map.
|
||||
/// </summary>
|
||||
public Location Center
|
||||
{
|
||||
get => (Location)GetValue(CenterProperty);
|
||||
set => SetValue(CenterProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target value of a Center animation.
|
||||
/// </summary>
|
||||
public Location TargetCenter
|
||||
{
|
||||
get => (Location)GetValue(TargetCenterProperty);
|
||||
set => SetValue(TargetCenterProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public double MinZoomLevel
|
||||
{
|
||||
get => (double)GetValue(MinZoomLevelProperty);
|
||||
set => SetValue(MinZoomLevelProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum value of the ZoomLevel and TargetZoomLevel properties.
|
||||
/// Must not be less than MinZoomLevel. The default value is 20.
|
||||
/// </summary>
|
||||
public double MaxZoomLevel
|
||||
{
|
||||
get => (double)GetValue(MaxZoomLevelProperty);
|
||||
set => SetValue(MaxZoomLevelProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the map zoom level.
|
||||
/// </summary>
|
||||
public double ZoomLevel
|
||||
{
|
||||
get => (double)GetValue(ZoomLevelProperty);
|
||||
set => SetValue(ZoomLevelProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target value of a ZoomLevel animation.
|
||||
/// </summary>
|
||||
public double TargetZoomLevel
|
||||
{
|
||||
get => (double)GetValue(TargetZoomLevelProperty);
|
||||
set => SetValue(TargetZoomLevelProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the map heading, a counter-clockwise rotation angle in degrees.
|
||||
/// </summary>
|
||||
public double Heading
|
||||
{
|
||||
get => (double)GetValue(HeadingProperty);
|
||||
set => SetValue(HeadingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target value of a Heading animation.
|
||||
/// </summary>
|
||||
public double TargetHeading
|
||||
{
|
||||
get => (double)GetValue(TargetHeadingProperty);
|
||||
set => SetValue(TargetHeadingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ViewTransform instance that is used to transform between projected
|
||||
/// map coordinates and view coordinates.
|
||||
/// </summary>
|
||||
public ViewTransform ViewTransform { get; } = new ViewTransform();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Point GetScale(Location location)
|
||||
{
|
||||
var relativeScale = MapProjection.GetRelativeScale(location);
|
||||
|
||||
return new Point(ViewTransform.Scale * relativeScale.X, ViewTransform.Scale * relativeScale.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a Location in geographic coordinates to a Point in view coordinates.
|
||||
/// </summary>
|
||||
public Point? LocationToView(Location location)
|
||||
{
|
||||
var point = MapProjection.LocationToMap(location);
|
||||
|
||||
if (!point.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ViewTransform.MapToView(point.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a Point in view coordinates to a Location in geographic coordinates.
|
||||
/// </summary>
|
||||
public Location ViewToLocation(Point point)
|
||||
{
|
||||
return MapProjection.MapToLocation(ViewTransform.ViewToMap(point));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a Rect in view coordinates to a BoundingBox in geographic coordinates.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void SetTransformCenter(Point center)
|
||||
{
|
||||
transformCenter = ViewToLocation(center);
|
||||
viewCenter = transformCenter != null ? center : new Point(RenderSize.Width / 2d, RenderSize.Height / 2d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the temporary transform center point set by SetTransformCenter.
|
||||
/// </summary>
|
||||
public void ResetTransformCenter()
|
||||
{
|
||||
transformCenter = null;
|
||||
viewCenter = new Point(RenderSize.Width / 2d, RenderSize.Height / 2d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the Center property according to the specified translation in view coordinates.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of the TargetZoomLevel property while retaining the specified center point
|
||||
/// in view coordinates.
|
||||
/// </summary>
|
||||
public void ZoomMap(Point center, double zoomLevel)
|
||||
{
|
||||
zoomLevel = CoerceZoomLevelProperty(zoomLevel);
|
||||
|
||||
if (TargetZoomLevel != zoomLevel)
|
||||
{
|
||||
SetTransformCenter(center);
|
||||
|
||||
TargetZoomLevel = zoomLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the TargetZoomLevel and TargetCenter properties so that the specified bounding box
|
||||
/// fits into the current view. The TargetHeading property is set to zero.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public partial class MapBase : MapPanel
|
||||
{
|
||||
public static TimeSpan ImageFadeDuration { get; set; } = TimeSpan.FromSeconds(0.1);
|
||||
|
||||
public static readonly DependencyProperty ForegroundProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, Brush>(nameof(Foreground), new SolidColorBrush(Colors.Black));
|
||||
|
||||
public static readonly DependencyProperty AnimationDurationProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, TimeSpan>(nameof(AnimationDuration), TimeSpan.FromSeconds(0.3));
|
||||
|
||||
public static readonly DependencyProperty MapLayerProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, UIElement>(nameof(MapLayer), null, false,
|
||||
(map, oldValue, newValue) => map.MapLayerPropertyChanged(oldValue, newValue));
|
||||
|
||||
public static readonly DependencyProperty MapProjectionProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, MapProjection>(nameof(MapProjection), new WebMercatorProjection(), false,
|
||||
(map, oldValue, newValue) => map.MapProjectionPropertyChanged(newValue));
|
||||
|
||||
public static readonly DependencyProperty ProjectionCenterProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, Location>(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;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the current map viewport has changed.
|
||||
/// </summary>
|
||||
public event EventHandler<ViewportChangedEventArgs> ViewportChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the map foreground Brush.
|
||||
/// </summary>
|
||||
public Brush Foreground
|
||||
{
|
||||
get => (Brush)GetValue(ForegroundProperty);
|
||||
set => SetValue(ForegroundProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Duration of the Center, ZoomLevel and Heading animations.
|
||||
/// The default value is 0.3 seconds.
|
||||
/// </summary>
|
||||
public TimeSpan AnimationDuration
|
||||
{
|
||||
get => (TimeSpan)GetValue(AnimationDurationProperty);
|
||||
set => SetValue(AnimationDurationProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public UIElement MapLayer
|
||||
{
|
||||
get => (UIElement)GetValue(MapLayerProperty);
|
||||
set => SetValue(MapLayerProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MapProjection used by the map control.
|
||||
/// </summary>
|
||||
public MapProjection MapProjection
|
||||
{
|
||||
get => (MapProjection)GetValue(MapProjectionProperty);
|
||||
set => SetValue(MapProjectionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional center (reference point) for azimuthal projections.
|
||||
/// If ProjectionCenter is null, the Center property value will be used instead.
|
||||
/// </summary>
|
||||
public Location ProjectionCenter
|
||||
{
|
||||
get => (Location)GetValue(ProjectionCenterProperty);
|
||||
set => SetValue(ProjectionCenterProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location of the center point of the map.
|
||||
/// </summary>
|
||||
public Location Center
|
||||
{
|
||||
get => (Location)GetValue(CenterProperty);
|
||||
set => SetValue(CenterProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target value of a Center animation.
|
||||
/// </summary>
|
||||
public Location TargetCenter
|
||||
{
|
||||
get => (Location)GetValue(TargetCenterProperty);
|
||||
set => SetValue(TargetCenterProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public double MinZoomLevel
|
||||
{
|
||||
get => (double)GetValue(MinZoomLevelProperty);
|
||||
set => SetValue(MinZoomLevelProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum value of the ZoomLevel and TargetZoomLevel properties.
|
||||
/// Must not be less than MinZoomLevel. The default value is 20.
|
||||
/// </summary>
|
||||
public double MaxZoomLevel
|
||||
{
|
||||
get => (double)GetValue(MaxZoomLevelProperty);
|
||||
set => SetValue(MaxZoomLevelProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the map zoom level.
|
||||
/// </summary>
|
||||
public double ZoomLevel
|
||||
{
|
||||
get => (double)GetValue(ZoomLevelProperty);
|
||||
set => SetValue(ZoomLevelProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target value of a ZoomLevel animation.
|
||||
/// </summary>
|
||||
public double TargetZoomLevel
|
||||
{
|
||||
get => (double)GetValue(TargetZoomLevelProperty);
|
||||
set => SetValue(TargetZoomLevelProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the map heading, a counter-clockwise rotation angle in degrees.
|
||||
/// </summary>
|
||||
public double Heading
|
||||
{
|
||||
get => (double)GetValue(HeadingProperty);
|
||||
set => SetValue(HeadingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target value of a Heading animation.
|
||||
/// </summary>
|
||||
public double TargetHeading
|
||||
{
|
||||
get => (double)GetValue(TargetHeadingProperty);
|
||||
set => SetValue(TargetHeadingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ViewTransform instance that is used to transform between projected
|
||||
/// map coordinates and view coordinates.
|
||||
/// </summary>
|
||||
public ViewTransform ViewTransform { get; } = new ViewTransform();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Point GetScale(Location location)
|
||||
{
|
||||
var relativeScale = MapProjection.GetRelativeScale(location);
|
||||
|
||||
return new Point(ViewTransform.Scale * relativeScale.X, ViewTransform.Scale * relativeScale.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a Location in geographic coordinates to a Point in view coordinates.
|
||||
/// </summary>
|
||||
public Point? LocationToView(Location location)
|
||||
{
|
||||
var point = MapProjection.LocationToMap(location);
|
||||
|
||||
if (!point.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ViewTransform.MapToView(point.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a Point in view coordinates to a Location in geographic coordinates.
|
||||
/// </summary>
|
||||
public Location ViewToLocation(Point point)
|
||||
{
|
||||
return MapProjection.MapToLocation(ViewTransform.ViewToMap(point));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a Rect in view coordinates to a BoundingBox in geographic coordinates.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void SetTransformCenter(Point center)
|
||||
{
|
||||
transformCenter = ViewToLocation(center);
|
||||
viewCenter = transformCenter != null ? center : new Point(RenderSize.Width / 2d, RenderSize.Height / 2d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the temporary transform center point set by SetTransformCenter.
|
||||
/// </summary>
|
||||
public void ResetTransformCenter()
|
||||
{
|
||||
transformCenter = null;
|
||||
viewCenter = new Point(RenderSize.Width / 2d, RenderSize.Height / 2d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the Center property according to the specified translation in view coordinates.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of the TargetZoomLevel property while retaining the specified center point
|
||||
/// in view coordinates.
|
||||
/// </summary>
|
||||
public void ZoomMap(Point center, double zoomLevel)
|
||||
{
|
||||
zoomLevel = Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
|
||||
|
||||
if (TargetZoomLevel != zoomLevel)
|
||||
{
|
||||
SetTransformCenter(center);
|
||||
|
||||
TargetZoomLevel = zoomLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the TargetZoomLevel and TargetCenter properties so that the specified bounding box
|
||||
/// fits into the current view. The TargetHeading property is set to zero.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -68,9 +68,6 @@
|
|||
<Compile Include="..\Shared\CenteredBoundingBox.cs">
|
||||
<Link>CenteredBoundingBox.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Shared\DependencyPropertyHelper.cs">
|
||||
<Link>DependencyPropertyHelper.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Shared\EquirectangularProjection.cs">
|
||||
<Link>EquirectangularProjection.cs</Link>
|
||||
</Compile>
|
||||
|
|
@ -107,9 +104,6 @@
|
|||
<Compile Include="..\Shared\MapBase.cs">
|
||||
<Link>MapBase.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Shared\MapBaseCommon.cs">
|
||||
<Link>MapBaseCommon.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Shared\MapBorderPanel.cs">
|
||||
<Link>MapBorderPanel.cs</Link>
|
||||
</Compile>
|
||||
|
|
@ -230,6 +224,9 @@
|
|||
<Compile Include="..\WinUI\Animatable.WinUI.cs">
|
||||
<Link>Animatable.WinUI.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\WinUI\DependencyPropertyHelper.WinUI.cs">
|
||||
<Link>DependencyPropertyHelper.WinUI.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\WinUI\GeoImage.WinUI.cs">
|
||||
<Link>GeoImage.WinUI.cs</Link>
|
||||
</Compile>
|
||||
|
|
|
|||
|
|
@ -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<TOwner, TValue, TValue> propertyChanged = null)
|
||||
Action<TOwner, TValue, TValue> changed = null,
|
||||
Func<TOwner, TValue, TValue> 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<FrameworkElement, TValue, TValue> propertyChanged = null)
|
||||
Action<FrameworkElement, TValue, TValue> 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<TOwner, TValue>(
|
||||
string name,
|
||||
TValue defaultValue = default)
|
||||
where TOwner : DependencyObject
|
||||
{
|
||||
return DependencyProperty.RegisterReadOnly(name, typeof(TValue), typeof(TOwner), new PropertyMetadata(defaultValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
43
MapControl/WPF/LocationAnimation.cs
Normal file
43
MapControl/WPF/LocationAnimation.cs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MapBase, IEasingFunction>(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<MapBase, Location>(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<MapBase, Location>(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<MapBase, double>(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<MapBase, double>(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<MapBase, double>(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<MapBase, double>(nameof(TargetZoomLevel), 1d, true,
|
||||
(map, oldValue, newValue) => map.TargetZoomLevelPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceZoomLevelProperty(value));
|
||||
|
||||
public static readonly DependencyProperty HeadingProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(Heading), 0d, true,
|
||||
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceHeadingProperty(value));
|
||||
|
||||
public static readonly DependencyProperty TargetHeadingProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d, true,
|
||||
(map, oldValue, newValue) => map.TargetHeadingPropertyChanged(newValue),
|
||||
(map, value) => map.CoerceHeadingProperty(value));
|
||||
|
||||
private static readonly DependencyPropertyKey ViewScalePropertyKey =
|
||||
DependencyPropertyHelper.RegisterReadOnly<MapBase, double>(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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
|
||||
/// The default value is a QuadraticEase with EasingMode.EaseOut.
|
||||
/// </summary>
|
||||
public IEasingFunction AnimationEasingFunction
|
||||
{
|
||||
get => (IEasingFunction)GetValue(AnimationEasingFunctionProperty);
|
||||
set => SetValue(AnimationEasingFunctionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scaling factor from projected map coordinates to view coordinates,
|
||||
/// as pixels per meter.
|
||||
/// </summary>
|
||||
public double ViewScale => (double)GetValue(ViewScaleProperty);
|
||||
|
||||
/// <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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
MapControl/WinUI/DependencyPropertyHelper.WinUI.cs
Normal file
44
MapControl/WinUI/DependencyPropertyHelper.WinUI.cs
Normal file
|
|
@ -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<TOwner, TValue>(
|
||||
string name,
|
||||
TValue defaultValue = default,
|
||||
bool bindTwoWayByDefault = false,
|
||||
Action<TOwner, TValue, TValue> 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<TOwner, TValue>(
|
||||
string name,
|
||||
TValue defaultValue = default,
|
||||
bool inherits = false,
|
||||
Action<FrameworkElement, TValue, TValue> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MapBase, EasingFunctionBase>(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<MapBase, Location>(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<MapBase, Location>(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<MapBase, double>(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<MapBase, double>(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<MapBase, double>(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<MapBase, double>(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<MapBase, double>(nameof(Heading), 0d, true,
|
||||
(map, oldValue, newValue) => map.HeadingPropertyChanged(newValue));
|
||||
|
||||
public static readonly DependencyProperty TargetHeadingProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(TargetHeading), 0d, true,
|
||||
(map, oldValue, newValue) => map.TargetHeadingPropertyChanged(newValue));
|
||||
|
||||
public static readonly DependencyProperty ViewScaleProperty =
|
||||
DependencyPropertyHelper.Register<MapBase, double>(nameof(ViewScale), 0d);
|
||||
|
||||
private PointAnimation centerAnimation;
|
||||
private DoubleAnimation zoomLevelAnimation;
|
||||
private DoubleAnimation headingAnimation;
|
||||
|
||||
public MapBase()
|
||||
{
|
||||
|
|
@ -62,6 +72,36 @@ namespace MapControl
|
|||
SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
|
||||
/// The default value is a QuadraticEase with EasingMode.EaseOut.
|
||||
/// </summary>
|
||||
public EasingFunctionBase AnimationEasingFunction
|
||||
{
|
||||
get => (EasingFunctionBase)GetValue(AnimationEasingFunctionProperty);
|
||||
set => SetValue(AnimationEasingFunctionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scaling factor from projected map coordinates to view coordinates,
|
||||
/// as pixels per meter.
|
||||
/// </summary>
|
||||
public double ViewScale => (double)GetValue(ViewScaleProperty);
|
||||
|
||||
/// <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);
|
||||
|
||||
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<MapBase, Windows.Foundation.Point>("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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue