mirror of
https://github.com/ClemensFischer/XAML-Map-Control.git
synced 2026-04-20 22:05:07 +00:00
Added DependencyPropertyHelper
This commit is contained in:
parent
422f1dce0d
commit
3706709cfc
22 changed files with 967 additions and 1879 deletions
47
MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs
Normal file
47
MapControl/Avalonia/DependencyPropertyHelper.Avalonia.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MapControl
|
||||||
|
{
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001")]
|
||||||
|
public static class DependencyPropertyHelper
|
||||||
|
{
|
||||||
|
public static AvaloniaProperty Register<TOwner, TValue>(
|
||||||
|
string name,
|
||||||
|
TValue defaultValue = default,
|
||||||
|
bool bindTwoWayByDefault = false,
|
||||||
|
Action<TOwner, TValue, TValue> propertyChanged = null)
|
||||||
|
where TOwner : AvaloniaObject
|
||||||
|
{
|
||||||
|
StyledProperty<TValue> property = AvaloniaProperty.Register<TOwner, TValue>(name, defaultValue, false,
|
||||||
|
bindTwoWayByDefault ? Avalonia.Data.BindingMode.TwoWay : Avalonia.Data.BindingMode.OneWay);
|
||||||
|
|
||||||
|
if (propertyChanged != null)
|
||||||
|
{
|
||||||
|
property.Changed.AddClassHandler<TOwner, TValue>(
|
||||||
|
(o, e) => propertyChanged(o, e.OldValue.Value, e.NewValue.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AvaloniaProperty RegisterAttached<TOwner, TValue>(
|
||||||
|
string name,
|
||||||
|
TValue defaultValue = default,
|
||||||
|
bool inherits = false,
|
||||||
|
Action<Control, TValue, TValue> propertyChanged = null)
|
||||||
|
where TOwner : AvaloniaObject
|
||||||
|
{
|
||||||
|
AttachedProperty<TValue> property = AvaloniaProperty.RegisterAttached<TOwner, Control, TValue>(name, defaultValue, inherits);
|
||||||
|
|
||||||
|
if (propertyChanged != null)
|
||||||
|
{
|
||||||
|
property.Changed.AddClassHandler<Control, TValue>(
|
||||||
|
(o, e) => propertyChanged(o, e.OldValue.Value, e.NewValue.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
MapControl/Avalonia/LocationAnimator.Avalonia.cs
Normal file
16
MapControl/Avalonia/LocationAnimator.Avalonia.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
|
||||||
|
// Copyright © 2024 Clemens Fischer
|
||||||
|
// Licensed under the Microsoft Public License (Ms-PL)
|
||||||
|
|
||||||
|
using Avalonia.Animation;
|
||||||
|
|
||||||
|
namespace MapControl
|
||||||
|
{
|
||||||
|
public class LocationAnimator : InterpolatingAnimator<Location>
|
||||||
|
{
|
||||||
|
public override Location Interpolate(double progress, Location oldValue, Location newValue)
|
||||||
|
{
|
||||||
|
throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
// Licensed under the Microsoft Public License (Ms-PL)
|
// Licensed under the Microsoft Public License (Ms-PL)
|
||||||
|
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
{
|
{
|
||||||
|
|
@ -16,16 +15,6 @@ namespace MapControl
|
||||||
= AvaloniaProperty.Register<Map, double>(nameof(MouseWheelZoomDelta), 0.25);
|
= AvaloniaProperty.Register<Map, double>(nameof(MouseWheelZoomDelta), 0.25);
|
||||||
|
|
||||||
private Point? mousePosition;
|
private Point? mousePosition;
|
||||||
private double targetZoomLevel;
|
|
||||||
private CancellationTokenSource cancellationTokenSource;
|
|
||||||
|
|
||||||
public Map()
|
|
||||||
{
|
|
||||||
PointerWheelChanged += OnPointerWheelChanged;
|
|
||||||
PointerPressed += OnPointerPressed;
|
|
||||||
PointerReleased += OnPointerReleased;
|
|
||||||
PointerMoved += OnPointerMoved;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the amount by which the ZoomLevel property changes by a MouseWheel event.
|
/// Gets or sets the amount by which the ZoomLevel property changes by a MouseWheel event.
|
||||||
|
|
@ -37,30 +26,17 @@ namespace MapControl
|
||||||
set => SetValue(MouseWheelZoomDeltaProperty, value);
|
set => SetValue(MouseWheelZoomDeltaProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnPointerWheelChanged(object sender, PointerWheelEventArgs e)
|
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
|
||||||
{
|
{
|
||||||
var delta = MouseWheelZoomDelta * e.Delta.Y;
|
base.OnPointerWheelChanged(e);
|
||||||
|
|
||||||
if (cancellationTokenSource != null)
|
ZoomMap(e.GetPosition(this), TargetZoomLevel + MouseWheelZoomDelta * e.Delta.Y);
|
||||||
{
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
targetZoomLevel += delta;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
targetZoomLevel = ZoomLevel + delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
await ZoomMap(e.GetPosition(this), targetZoomLevel, cancellationTokenSource.Token);
|
|
||||||
|
|
||||||
cancellationTokenSource.Dispose();
|
|
||||||
cancellationTokenSource = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPointerPressed(object sender, PointerPressedEventArgs e)
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||||
{
|
{
|
||||||
|
base.OnPointerPressed(e);
|
||||||
|
|
||||||
var point = e.GetCurrentPoint(this);
|
var point = e.GetCurrentPoint(this);
|
||||||
|
|
||||||
if (point.Properties.IsLeftButtonPressed)
|
if (point.Properties.IsLeftButtonPressed)
|
||||||
|
|
@ -70,8 +46,10 @@ namespace MapControl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPointerReleased(object sender, PointerReleasedEventArgs e)
|
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||||
{
|
{
|
||||||
|
base.OnPointerReleased(e);
|
||||||
|
|
||||||
if (mousePosition.HasValue)
|
if (mousePosition.HasValue)
|
||||||
{
|
{
|
||||||
e.Pointer.Capture(null);
|
e.Pointer.Capture(null);
|
||||||
|
|
@ -79,8 +57,10 @@ namespace MapControl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPointerMoved(object sender, PointerEventArgs e)
|
protected override void OnPointerMoved(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
|
base.OnPointerMoved(e);
|
||||||
|
|
||||||
if (mousePosition.HasValue)
|
if (mousePosition.HasValue)
|
||||||
{
|
{
|
||||||
var position = e.GetPosition(this);
|
var position = e.GetPosition(this);
|
||||||
|
|
|
||||||
|
|
@ -6,44 +6,23 @@ global using Avalonia;
|
||||||
using Avalonia.Animation;
|
using Avalonia.Animation;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
using Avalonia.Media;
|
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
{
|
{
|
||||||
public interface IMapLayer
|
public partial class MapBase
|
||||||
{
|
{
|
||||||
IBrush MapBackground { get; }
|
|
||||||
IBrush MapForeground { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MapBase : MapPanel
|
|
||||||
{
|
|
||||||
public static TimeSpan ImageFadeDuration { get; set; } = TimeSpan.FromSeconds(0.1);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> ForegroundProperty
|
|
||||||
= AvaloniaProperty.Register<MapBase, IBrush>(nameof(Foreground));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<TimeSpan> AnimationDurationProperty
|
|
||||||
= AvaloniaProperty.Register<MapBase, TimeSpan>(nameof(AnimationDuration), TimeSpan.FromSeconds(0.3));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<Control> MapLayerProperty
|
|
||||||
= AvaloniaProperty.Register<MapBase, Control>(nameof(MapLayer));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<MapProjection> MapProjectionProperty
|
|
||||||
= AvaloniaProperty.Register<MapBase, MapProjection>(nameof(MapProjection), new WebMercatorProjection());
|
|
||||||
|
|
||||||
public static readonly StyledProperty<Location> ProjectionCenterProperty
|
|
||||||
= AvaloniaProperty.Register<MapBase, Location>(nameof(ProjectionCenter));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<Location> CenterProperty
|
public static readonly StyledProperty<Location> CenterProperty
|
||||||
= AvaloniaProperty.Register<MapBase, Location>(nameof(Center), new Location(), false,
|
= AvaloniaProperty.Register<MapBase, Location>(nameof(Center), new Location(), false,
|
||||||
BindingMode.TwoWay, null, (map, center) => ((MapBase)map).CoerceCenterProperty(center));
|
BindingMode.TwoWay, null, (map, center) => ((MapBase)map).CoerceCenterProperty(center));
|
||||||
|
|
||||||
|
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<double> MinZoomLevelProperty
|
public static readonly StyledProperty<double> MinZoomLevelProperty
|
||||||
= AvaloniaProperty.Register<MapBase, double>(nameof(MinZoomLevel), 1d, false,
|
= AvaloniaProperty.Register<MapBase, double>(nameof(MinZoomLevel), 1d, false,
|
||||||
BindingMode.OneWay, null, (map, minZoomLevel) => ((MapBase)map).CoerceMinZoomLevelProperty(minZoomLevel));
|
BindingMode.OneWay, null, (map, minZoomLevel) => ((MapBase)map).CoerceMinZoomLevelProperty(minZoomLevel));
|
||||||
|
|
@ -56,37 +35,46 @@ namespace MapControl
|
||||||
= AvaloniaProperty.Register<MapBase, double>(nameof(ZoomLevel), 1d, false,
|
= AvaloniaProperty.Register<MapBase, double>(nameof(ZoomLevel), 1d, false,
|
||||||
BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel));
|
BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel));
|
||||||
|
|
||||||
|
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> HeadingProperty
|
public static readonly StyledProperty<double> HeadingProperty
|
||||||
= AvaloniaProperty.Register<MapBase, double>(nameof(Heading), 0d, false,
|
= AvaloniaProperty.Register<MapBase, double>(nameof(Heading), 0d, false,
|
||||||
BindingMode.TwoWay, null, (map, heading) => ((heading % 360d) + 360d) % 360d);
|
BindingMode.TwoWay, null, (map, heading) => CoerceHeadingProperty(heading));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> TargetHeadingProperty
|
||||||
|
= AvaloniaProperty.Register<MapBase, double>(nameof(TargetHeading), 0d, false,
|
||||||
|
BindingMode.TwoWay, null, (map, heading) => CoerceHeadingProperty(heading));
|
||||||
|
|
||||||
public static readonly DirectProperty<MapBase, double> ViewScaleProperty
|
public static readonly DirectProperty<MapBase, double> ViewScaleProperty
|
||||||
= AvaloniaProperty.RegisterDirect<MapBase, double>(nameof(ViewScale), map => map.ViewScale);
|
= AvaloniaProperty.RegisterDirect<MapBase, double>(nameof(ViewScale), map => map.ViewScale);
|
||||||
|
|
||||||
private Location transformCenter;
|
private CancellationTokenSource centerCts;
|
||||||
private Point viewCenter;
|
private CancellationTokenSource zoomLevelCts;
|
||||||
private double centerLongitude;
|
private CancellationTokenSource headingCts;
|
||||||
private double maxLatitude = 90d;
|
private Animation centerAnimation;
|
||||||
|
private Animation zoomLevelAnimation;
|
||||||
|
private Animation headingAnimation;
|
||||||
|
|
||||||
static MapBase()
|
static MapBase()
|
||||||
{
|
{
|
||||||
MapLayerProperty.Changed.AddClassHandler<MapBase, Control>(
|
ClipToBoundsProperty.OverrideDefaultValue(typeof(MapBase), true);
|
||||||
(map, args) => map.MapLayerPropertyChanged(args));
|
|
||||||
|
|
||||||
MapProjectionProperty.Changed.AddClassHandler<MapBase, MapProjection>(
|
|
||||||
(map, args) => map.MapProjectionPropertyChanged(args.NewValue.Value));
|
|
||||||
|
|
||||||
ProjectionCenterProperty.Changed.AddClassHandler<MapBase, Location>(
|
|
||||||
(map, args) => map.ProjectionCenterPropertyChanged());
|
|
||||||
|
|
||||||
CenterProperty.Changed.AddClassHandler<MapBase, Location>(
|
CenterProperty.Changed.AddClassHandler<MapBase, Location>(
|
||||||
(map, args) => map.UpdateTransform());
|
(map, args) => map.CenterPropertyChanged(args.NewValue.Value));
|
||||||
|
|
||||||
ZoomLevelProperty.Changed.AddClassHandler<MapBase, double>(
|
ZoomLevelProperty.Changed.AddClassHandler<MapBase, double>(
|
||||||
(map, args) => map.UpdateTransform());
|
(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>(
|
HeadingProperty.Changed.AddClassHandler<MapBase, double>(
|
||||||
(map, args) => map.UpdateTransform());
|
(map, args) => map.HeadingPropertyChanged(args.NewValue.Value));
|
||||||
|
|
||||||
|
TargetHeadingProperty.Changed.AddClassHandler<MapBase, double>(
|
||||||
|
async (map, args) => await map.TargetHeadingPropertyChanged(args.NewValue.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
public MapBase()
|
public MapBase()
|
||||||
|
|
@ -94,105 +82,14 @@ namespace MapControl
|
||||||
MapProjectionPropertyChanged(MapProjection);
|
MapProjectionPropertyChanged(MapProjection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
internal Size RenderSize => Bounds.Size;
|
||||||
/// Raised when the current map viewport has changed.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<ViewportChangedEventArgs> ViewportChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
protected override void OnSizeChanged(SizeChangedEventArgs e)
|
||||||
/// Gets or sets the map foreground Brush.
|
|
||||||
/// </summary>
|
|
||||||
public IBrush Foreground
|
|
||||||
{
|
{
|
||||||
get => GetValue(ForegroundProperty);
|
base.OnSizeChanged(e);
|
||||||
set => SetValue(ForegroundProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
ResetTransformCenter();
|
||||||
/// Gets or sets the Duration of the Center, ZoomLevel and Heading animations.
|
UpdateTransform();
|
||||||
/// The default value is 0.3 seconds.
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan AnimationDuration
|
|
||||||
{
|
|
||||||
get => 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 Control MapLayer
|
|
||||||
{
|
|
||||||
get => GetValue(MapLayerProperty);
|
|
||||||
set => SetValue(MapLayerProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the MapProjection used by the map control.
|
|
||||||
/// </summary>
|
|
||||||
public MapProjection MapProjection
|
|
||||||
{
|
|
||||||
get => 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 => GetValue(ProjectionCenterProperty);
|
|
||||||
set => SetValue(ProjectionCenterProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the location of the center point of the map.
|
|
||||||
/// </summary>
|
|
||||||
public Location Center
|
|
||||||
{
|
|
||||||
get => GetValue(CenterProperty);
|
|
||||||
set => SetValue(CenterProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the minimum value of the ZoomLevel property.
|
|
||||||
/// Must not be less than zero or greater than MaxZoomLevel. The default value is 1.
|
|
||||||
/// </summary>
|
|
||||||
public double MinZoomLevel
|
|
||||||
{
|
|
||||||
get => GetValue(MinZoomLevelProperty);
|
|
||||||
set => SetValue(MinZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the maximum value of the ZoomLevel property.
|
|
||||||
/// Must not be less than MinZoomLevel. The default value is 20.
|
|
||||||
/// </summary>
|
|
||||||
public double MaxZoomLevel
|
|
||||||
{
|
|
||||||
get => GetValue(MaxZoomLevelProperty);
|
|
||||||
set => SetValue(MaxZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the map zoom level.
|
|
||||||
/// </summary>
|
|
||||||
public double ZoomLevel
|
|
||||||
{
|
|
||||||
get => GetValue(ZoomLevelProperty);
|
|
||||||
set => SetValue(ZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the map heading, a counter-clockwise rotation angle in degrees.
|
|
||||||
/// </summary>
|
|
||||||
public double Heading
|
|
||||||
{
|
|
||||||
get => GetValue(HeadingProperty);
|
|
||||||
set => SetValue(HeadingProperty, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -204,19 +101,9 @@ namespace MapControl
|
||||||
get => ViewTransform.Scale;
|
get => ViewTransform.Scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void SetViewScale(double viewScale)
|
||||||
/// 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)
|
|
||||||
{
|
{
|
||||||
return MapProjection.GetRelativeScale(location) * ViewTransform.Scale;
|
RaisePropertyChanged(ViewScaleProperty, double.NaN, viewScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -231,278 +118,6 @@ namespace MapControl
|
||||||
.Append(Matrix.CreateRotation(ViewTransform.Rotation));
|
.Append(Matrix.CreateRotation(ViewTransform.Rotation));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transforms a Location in geographic coordinates to a Point in view coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public Point? LocationToView(Location location)
|
|
||||||
{
|
|
||||||
var point = MapProjection.LocationToMap(location);
|
|
||||||
|
|
||||||
return point.HasValue ? ViewTransform.MapToView(point.Value) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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, y2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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(Bounds.Width / 2d, Bounds.Height / 2d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resets the temporary transform center point set by SetTransformCenter.
|
|
||||||
/// </summary>
|
|
||||||
public void ResetTransformCenter()
|
|
||||||
{
|
|
||||||
transformCenter = null;
|
|
||||||
viewCenter = new Point(Bounds.Width / 2d, Bounds.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 += translation;
|
|
||||||
|
|
||||||
if (rotation != 0d)
|
|
||||||
{
|
|
||||||
Heading = (((Heading - rotation) % 360d) + 360d) % 360d;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scale != 1d)
|
|
||||||
{
|
|
||||||
ZoomLevel = Math.Min(Math.Max(ZoomLevel + Math.Log(scale, 2d), MinZoomLevel), MaxZoomLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateTransform(true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// More accurate than SetTransformCenter.
|
|
||||||
//
|
|
||||||
TranslateMap(translation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Animates the value of the ZoomLevel property while retaining the specified center point
|
|
||||||
/// in view coordinates.
|
|
||||||
/// </summary>
|
|
||||||
public async Task ZoomMap(Point center, double zoomLevel, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
zoomLevel = Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
|
|
||||||
|
|
||||||
if (zoomLevel != ZoomLevel)
|
|
||||||
{
|
|
||||||
SetTransformCenter(center);
|
|
||||||
|
|
||||||
var animation = new Animation
|
|
||||||
{
|
|
||||||
FillMode = FillMode.Forward,
|
|
||||||
Duration = AnimationDuration,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new KeyFrame
|
|
||||||
{
|
|
||||||
KeyTime = AnimationDuration,
|
|
||||||
Setters = { new Setter(ZoomLevelProperty, zoomLevel) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await animation.RunAsync(this, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnSizeChanged(SizeChangedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnSizeChanged(e);
|
|
||||||
|
|
||||||
ResetTransformCenter();
|
|
||||||
UpdateTransform();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 UpdateTransform(bool resetTransformCenter = false, bool projectionChanged = false)
|
|
||||||
{
|
|
||||||
var transformCenterChanged = false;
|
|
||||||
var oldViewScale = ViewScale;
|
|
||||||
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(Bounds.Width / 2d, Bounds.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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RaisePropertyChanged(ViewScaleProperty, oldViewScale, ViewScale);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MapLayerPropertyChanged(AvaloniaPropertyChangedEventArgs<Control> args)
|
|
||||||
{
|
|
||||||
if (args.OldValue.Value != null)
|
|
||||||
{
|
|
||||||
Children.Remove(args.OldValue.Value);
|
|
||||||
|
|
||||||
if (args.OldValue.Value is IMapLayer mapLayer)
|
|
||||||
{
|
|
||||||
if (mapLayer.MapBackground != null)
|
|
||||||
{
|
|
||||||
ClearValue(BackgroundProperty);
|
|
||||||
}
|
|
||||||
if (mapLayer.MapForeground != null)
|
|
||||||
{
|
|
||||||
ClearValue(ForegroundProperty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.NewValue.Value != null)
|
|
||||||
{
|
|
||||||
Children.Insert(0, args.NewValue.Value);
|
|
||||||
|
|
||||||
if (args.NewValue.Value is IMapLayer mapLayer)
|
|
||||||
{
|
|
||||||
if (mapLayer.MapBackground != null)
|
|
||||||
{
|
|
||||||
Background = mapLayer.MapBackground;
|
|
||||||
}
|
|
||||||
if (mapLayer.MapForeground != null)
|
|
||||||
{
|
|
||||||
Foreground = mapLayer.MapForeground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MapProjectionPropertyChanged(MapProjection projection)
|
private void MapProjectionPropertyChanged(MapProjection projection)
|
||||||
{
|
{
|
||||||
maxLatitude = 90d;
|
maxLatitude = 90d;
|
||||||
|
|
@ -514,6 +129,7 @@ namespace MapControl
|
||||||
if (maxLocation != null && maxLocation.Latitude < 90d)
|
if (maxLocation != null && maxLocation.Latitude < 90d)
|
||||||
{
|
{
|
||||||
maxLatitude = maxLocation.Latitude;
|
maxLatitude = maxLocation.Latitude;
|
||||||
|
|
||||||
Center = CoerceCenterProperty(Center);
|
Center = CoerceCenterProperty(Center);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -522,12 +138,6 @@ namespace MapControl
|
||||||
UpdateTransform(false, true);
|
UpdateTransform(false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProjectionCenterPropertyChanged()
|
|
||||||
{
|
|
||||||
ResetTransformCenter();
|
|
||||||
UpdateTransform();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Location CoerceCenterProperty(Location center)
|
private Location CoerceCenterProperty(Location center)
|
||||||
{
|
{
|
||||||
if (center == null)
|
if (center == null)
|
||||||
|
|
@ -546,6 +156,14 @@ namespace MapControl
|
||||||
return center;
|
return center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CenterPropertyChanged(Location center)
|
||||||
|
{
|
||||||
|
if (!internalPropertyChange)
|
||||||
|
{
|
||||||
|
UpdateTransform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private double CoerceMinZoomLevelProperty(double minZoomLevel)
|
private double CoerceMinZoomLevelProperty(double minZoomLevel)
|
||||||
{
|
{
|
||||||
return Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel);
|
return Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel);
|
||||||
|
|
@ -560,5 +178,109 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
return Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
|
return Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ZoomLevelPropertyChanged(double zoomLevel)
|
||||||
|
{
|
||||||
|
if (!internalPropertyChange)
|
||||||
|
{
|
||||||
|
UpdateTransform();
|
||||||
|
|
||||||
|
if (zoomLevelAnimation == null)
|
||||||
|
{
|
||||||
|
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TargetZoomLevelPropertyChanged(double targetZoomLevel)
|
||||||
|
{
|
||||||
|
if (!internalPropertyChange && targetZoomLevel != ZoomLevel)
|
||||||
|
{
|
||||||
|
zoomLevelCts?.Cancel();
|
||||||
|
|
||||||
|
zoomLevelAnimation = new Animation
|
||||||
|
{
|
||||||
|
FillMode = FillMode.Forward,
|
||||||
|
Duration = AnimationDuration,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new KeyFrame
|
||||||
|
{
|
||||||
|
KeyTime = AnimationDuration,
|
||||||
|
Setters = { new Setter(ZoomLevelProperty, targetZoomLevel) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
zoomLevelCts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
await zoomLevelAnimation.RunAsync(this, zoomLevelCts.Token);
|
||||||
|
|
||||||
|
zoomLevelCts.Dispose();
|
||||||
|
zoomLevelCts = null;
|
||||||
|
zoomLevelAnimation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double CoerceHeadingProperty(double heading)
|
||||||
|
{
|
||||||
|
return ((heading % 360d) + 360d) % 360d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HeadingPropertyChanged(double heading)
|
||||||
|
{
|
||||||
|
if (!internalPropertyChange)
|
||||||
|
{
|
||||||
|
UpdateTransform();
|
||||||
|
|
||||||
|
if (headingAnimation == null)
|
||||||
|
{
|
||||||
|
SetValueInternal(TargetHeadingProperty, heading);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TargetHeadingPropertyChanged(double targetHeading)
|
||||||
|
{
|
||||||
|
if (!internalPropertyChange && targetHeading != Heading)
|
||||||
|
{
|
||||||
|
var delta = targetHeading - Heading;
|
||||||
|
|
||||||
|
if (delta > 180d)
|
||||||
|
{
|
||||||
|
delta -= 360d;
|
||||||
|
}
|
||||||
|
else if (delta < -180d)
|
||||||
|
{
|
||||||
|
delta += 360d;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetHeading = Heading + delta;
|
||||||
|
|
||||||
|
headingCts?.Cancel();
|
||||||
|
|
||||||
|
headingAnimation = new Animation
|
||||||
|
{
|
||||||
|
FillMode = FillMode.Forward,
|
||||||
|
Duration = AnimationDuration,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new KeyFrame
|
||||||
|
{
|
||||||
|
KeyTime = AnimationDuration,
|
||||||
|
Setters = { new Setter(HeadingProperty, targetHeading) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
headingCts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
await headingAnimation.RunAsync(this, headingCts.Token);
|
||||||
|
|
||||||
|
headingCts.Dispose();
|
||||||
|
headingCts = null;
|
||||||
|
headingAnimation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,11 @@
|
||||||
<Compile Include="..\Shared\ImageLoader.cs" Link="ImageLoader.cs" />
|
<Compile Include="..\Shared\ImageLoader.cs" Link="ImageLoader.cs" />
|
||||||
<Compile Include="..\Shared\Location.cs" Link="Location.cs" />
|
<Compile Include="..\Shared\Location.cs" Link="Location.cs" />
|
||||||
<Compile Include="..\Shared\LocationCollection.cs" Link="LocationCollection.cs" />
|
<Compile Include="..\Shared\LocationCollection.cs" Link="LocationCollection.cs" />
|
||||||
|
<Compile Include="..\Shared\MapBaseCommon.cs" Link="MapBaseCommon.cs" />
|
||||||
|
<Compile Include="..\Shared\MapPanel.cs" Link="MapPanel.cs" />
|
||||||
<Compile Include="..\Shared\MapProjection.cs" Link="MapProjection.cs" />
|
<Compile Include="..\Shared\MapProjection.cs" Link="MapProjection.cs" />
|
||||||
|
<Compile Include="..\Shared\MapTileLayer.cs" Link="MapTileLayer.cs" />
|
||||||
|
<Compile Include="..\Shared\MapTileLayerBase.cs" Link="MapTileLayerBase.cs" />
|
||||||
<Compile Include="..\Shared\Tile.cs" Link="Tile.cs" />
|
<Compile Include="..\Shared\Tile.cs" Link="Tile.cs" />
|
||||||
<Compile Include="..\Shared\TileCollection.cs" Link="TileCollection.cs" />
|
<Compile Include="..\Shared\TileCollection.cs" Link="TileCollection.cs" />
|
||||||
<Compile Include="..\Shared\TileImageLoader.cs" Link="TileImageLoader.cs" />
|
<Compile Include="..\Shared\TileImageLoader.cs" Link="TileImageLoader.cs" />
|
||||||
|
|
@ -30,6 +34,7 @@
|
||||||
<Compile Include="..\Shared\ViewportChangedEventArgs.cs" Link="ViewportChangedEventArgs.cs" />
|
<Compile Include="..\Shared\ViewportChangedEventArgs.cs" Link="ViewportChangedEventArgs.cs" />
|
||||||
<Compile Include="..\Shared\ViewRect.cs" Link="ViewRect.cs" />
|
<Compile Include="..\Shared\ViewRect.cs" Link="ViewRect.cs" />
|
||||||
<Compile Include="..\Shared\WebMercatorProjection.cs" Link="WebMercatorProjection.cs" />
|
<Compile Include="..\Shared\WebMercatorProjection.cs" Link="WebMercatorProjection.cs" />
|
||||||
|
<Compile Include="..\WPF\Timer.WPF.cs" Link="Timer.WPF.cs" />
|
||||||
<Compile Include="..\WPF\TypeConverters.WPF.cs" Link="TypeConverters.WPF.cs" />
|
<Compile Include="..\WPF\TypeConverters.WPF.cs" Link="TypeConverters.WPF.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,341 +4,35 @@
|
||||||
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
{
|
{
|
||||||
public class MapPanel : Panel
|
public partial class MapPanel
|
||||||
{
|
{
|
||||||
public static readonly AttachedProperty<MapBase> ParentMapProperty
|
|
||||||
= AvaloniaProperty.RegisterAttached<MapPanel, AvaloniaObject, MapBase>("ParentMap", null, true);
|
|
||||||
|
|
||||||
private static readonly AttachedProperty<Point?> ViewPositionProperty
|
|
||||||
= AvaloniaProperty.RegisterAttached<MapPanel, AvaloniaObject, Point?>("ViewPosition");
|
|
||||||
|
|
||||||
public static readonly AttachedProperty<Location> LocationProperty
|
|
||||||
= AvaloniaProperty.RegisterAttached<MapPanel, AvaloniaObject, Location>("Location");
|
|
||||||
|
|
||||||
public static readonly AttachedProperty<BoundingBox> BoundingBoxProperty
|
|
||||||
= AvaloniaProperty.RegisterAttached<MapPanel, AvaloniaObject, BoundingBox>("BoundingBox");
|
|
||||||
|
|
||||||
public static readonly AttachedProperty<bool> AutoCollapseProperty
|
|
||||||
= AvaloniaProperty.RegisterAttached<MapPanel, AvaloniaObject, bool>("AutoCollapse");
|
|
||||||
|
|
||||||
static MapPanel()
|
|
||||||
{
|
|
||||||
ParentMapProperty.Changed.AddClassHandler<MapPanel, MapBase>(
|
|
||||||
(panel, args) => panel.OnParentMapPropertyChanged(args.NewValue.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public MapPanel()
|
public MapPanel()
|
||||||
{
|
{
|
||||||
ClipToBounds = true;
|
|
||||||
|
|
||||||
if (this is MapBase mapBase)
|
if (this is MapBase mapBase)
|
||||||
{
|
{
|
||||||
SetParentMap(this, mapBase);
|
SetValue(ParentMapProperty, mapBase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MapBase ParentMap { get; private set; }
|
public static MapBase GetParentMap(Control element)
|
||||||
|
|
||||||
public static MapBase GetParentMap(AvaloniaObject obj) => obj.GetValue(ParentMapProperty);
|
|
||||||
|
|
||||||
private static void SetParentMap(AvaloniaObject obj, MapBase value) => obj.SetValue(ParentMapProperty, value);
|
|
||||||
|
|
||||||
public static Point? GetViewPosition(AvaloniaObject obj) => obj.GetValue(ViewPositionProperty);
|
|
||||||
|
|
||||||
private static void SetViewPosition(AvaloniaObject obj, Point? value) => obj.SetValue(ViewPositionProperty, value);
|
|
||||||
|
|
||||||
public static Location GetLocation(AvaloniaObject obj) => obj.GetValue(LocationProperty);
|
|
||||||
|
|
||||||
public static void SetLocation(AvaloniaObject obj, Location value) => obj.SetValue(LocationProperty, value);
|
|
||||||
|
|
||||||
public static BoundingBox GetBoundingBox(AvaloniaObject obj) => obj.GetValue(BoundingBoxProperty);
|
|
||||||
|
|
||||||
public static void SetBoundingBox(AvaloniaObject obj, BoundingBox value) => obj.SetValue(BoundingBoxProperty, value);
|
|
||||||
|
|
||||||
public static bool GetAutoCollapse(AvaloniaObject obj) => obj.GetValue(AutoCollapseProperty);
|
|
||||||
|
|
||||||
public static void SetAutoCollapse(AvaloniaObject obj, bool value) => obj.SetValue(AutoCollapseProperty, value);
|
|
||||||
|
|
||||||
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
|
|
||||||
{
|
{
|
||||||
InvalidateArrange();
|
return (MapBase)element.GetValue(ParentMapProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Size MeasureOverride(Size availableSize)
|
public static void SetRenderTransform(Control element, Transform transform, double originX = 0d, double originY = 0d)
|
||||||
{
|
{
|
||||||
availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
|
element.RenderTransform = transform;
|
||||||
|
element.RenderTransformOrigin = new RelativePoint(originX, originY, RelativeUnit.Relative);
|
||||||
foreach (var element in Children)
|
|
||||||
{
|
|
||||||
element.Measure(availableSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Size();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Size ArrangeOverride(Size finalSize)
|
private Controls ChildElements => Children;
|
||||||
|
|
||||||
|
private static void SetVisible(Control element, bool visible)
|
||||||
{
|
{
|
||||||
if (ParentMap != null)
|
element.IsVisible = visible;
|
||||||
{
|
|
||||||
foreach (var element in Children)
|
|
||||||
{
|
|
||||||
var location = GetLocation(element);
|
|
||||||
var position = location != null ? GetViewPosition(location) : null;
|
|
||||||
|
|
||||||
SetViewPosition(element, position);
|
|
||||||
|
|
||||||
if (GetAutoCollapse(element))
|
|
||||||
{
|
|
||||||
element.IsVisible = !(position.HasValue && IsOutsideViewport(position.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position.HasValue)
|
|
||||||
{
|
|
||||||
ArrangeElement(element, position.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var boundingBox = GetBoundingBox(element);
|
|
||||||
|
|
||||||
if (boundingBox != null)
|
|
||||||
{
|
|
||||||
var viewRect = GetViewRect(boundingBox);
|
|
||||||
|
|
||||||
if (viewRect.HasValue)
|
|
||||||
{
|
|
||||||
ArrangeElement(element, viewRect.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ArrangeElement(element, finalSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Point? GetViewPosition(Location location)
|
|
||||||
{
|
|
||||||
var position = ParentMap.LocationToView(location);
|
|
||||||
|
|
||||||
if (ParentMap.MapProjection.Type <= MapProjectionType.NormalCylindrical &&
|
|
||||||
position.HasValue &&
|
|
||||||
IsOutsideViewport(position.Value))
|
|
||||||
{
|
|
||||||
position = ParentMap.LocationToView(
|
|
||||||
new Location(location.Latitude, ParentMap.ConstrainedLongitude(location.Longitude)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ViewRect? GetViewRect(BoundingBox boundingBox)
|
|
||||||
{
|
|
||||||
var rect = ParentMap.MapProjection.BoundingBoxToMap(boundingBox);
|
|
||||||
|
|
||||||
if (!rect.HasValue)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetViewRect(rect.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ViewRect GetViewRect(Rect mapRect)
|
|
||||||
{
|
|
||||||
var position = ParentMap.ViewTransform.MapToView(mapRect.Center);
|
|
||||||
|
|
||||||
if (ParentMap.MapProjection.Type <= MapProjectionType.NormalCylindrical &&
|
|
||||||
IsOutsideViewport(position))
|
|
||||||
{
|
|
||||||
var location = ParentMap.MapProjection.MapToLocation(mapRect.Center);
|
|
||||||
|
|
||||||
if (location != null)
|
|
||||||
{
|
|
||||||
var pos = ParentMap.LocationToView(
|
|
||||||
new Location(location.Latitude, ParentMap.ConstrainedLongitude(location.Longitude)));
|
|
||||||
|
|
||||||
if (pos.HasValue)
|
|
||||||
{
|
|
||||||
position = pos.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var width = mapRect.Width * ParentMap.ViewTransform.Scale;
|
|
||||||
var height = mapRect.Height * ParentMap.ViewTransform.Scale;
|
|
||||||
var x = position.X - width / 2d;
|
|
||||||
var y = position.Y - height / 2d;
|
|
||||||
|
|
||||||
return new ViewRect(x, y, width, height, ParentMap.ViewTransform.Rotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsOutsideViewport(Point point)
|
|
||||||
{
|
|
||||||
return point.X < 0d || point.X > ParentMap.Bounds.Width
|
|
||||||
|| point.Y < 0d || point.Y > ParentMap.Bounds.Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ArrangeElement(Control element, Point position)
|
|
||||||
{
|
|
||||||
var size = GetDesiredSize(element);
|
|
||||||
var x = position.X;
|
|
||||||
var y = position.Y;
|
|
||||||
|
|
||||||
switch (element.HorizontalAlignment)
|
|
||||||
{
|
|
||||||
case Avalonia.Layout.HorizontalAlignment.Center:
|
|
||||||
x -= size.Width / 2d;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Avalonia.Layout.HorizontalAlignment.Right:
|
|
||||||
x -= size.Width;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (element.VerticalAlignment)
|
|
||||||
{
|
|
||||||
case Avalonia.Layout.VerticalAlignment.Center:
|
|
||||||
y -= size.Height / 2d;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Avalonia.Layout.VerticalAlignment.Bottom:
|
|
||||||
y -= size.Height;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrangeElement(element, new Rect(x, y, size.Width, size.Height));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ArrangeElement(Control element, Size parentSize)
|
|
||||||
{
|
|
||||||
var size = GetDesiredSize(element);
|
|
||||||
var x = 0d;
|
|
||||||
var y = 0d;
|
|
||||||
var width = size.Width;
|
|
||||||
var height = size.Height;
|
|
||||||
|
|
||||||
switch (element.HorizontalAlignment)
|
|
||||||
{
|
|
||||||
case Avalonia.Layout.HorizontalAlignment.Center:
|
|
||||||
x = (parentSize.Width - size.Width) / 2d;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Avalonia.Layout.HorizontalAlignment.Right:
|
|
||||||
x = parentSize.Width - size.Width;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Avalonia.Layout.HorizontalAlignment.Stretch:
|
|
||||||
width = parentSize.Width;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (element.VerticalAlignment)
|
|
||||||
{
|
|
||||||
case Avalonia.Layout.VerticalAlignment.Center:
|
|
||||||
y = (parentSize.Height - size.Height) / 2d;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Avalonia.Layout.VerticalAlignment.Bottom:
|
|
||||||
y = parentSize.Height - size.Height;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Avalonia.Layout.VerticalAlignment.Stretch:
|
|
||||||
height = parentSize.Height;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrangeElement(element, new Rect(x, y, width, height));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ArrangeElement(Control element, ViewRect rect)
|
|
||||||
{
|
|
||||||
element.Width = rect.Rect.Width;
|
|
||||||
element.Height = rect.Rect.Height;
|
|
||||||
|
|
||||||
ArrangeElement(element, rect.Rect);
|
|
||||||
|
|
||||||
if (element.RenderTransform is RotateTransform rotateTransform)
|
|
||||||
{
|
|
||||||
rotateTransform.Angle = rect.Rotation;
|
|
||||||
}
|
|
||||||
else if (rect.Rotation != 0d)
|
|
||||||
{
|
|
||||||
rotateTransform = new RotateTransform { Angle = rect.Rotation };
|
|
||||||
element.RenderTransform = rotateTransform;
|
|
||||||
element.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ArrangeElement(Control element, Rect rect)
|
|
||||||
{
|
|
||||||
if (element.UseLayoutRounding)
|
|
||||||
{
|
|
||||||
rect = new Rect(Math.Round(rect.X), Math.Round(rect.Y), Math.Round(rect.Width), Math.Round(rect.Height));
|
|
||||||
}
|
|
||||||
|
|
||||||
element.Arrange(rect);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Size GetDesiredSize(Control element)
|
|
||||||
{
|
|
||||||
var width = 0d;
|
|
||||||
var height = 0d;
|
|
||||||
|
|
||||||
if (element.DesiredSize.Width >= 0d &&
|
|
||||||
element.DesiredSize.Width < double.PositiveInfinity)
|
|
||||||
{
|
|
||||||
width = element.DesiredSize.Width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.DesiredSize.Height >= 0d &&
|
|
||||||
element.DesiredSize.Height < double.PositiveInfinity)
|
|
||||||
{
|
|
||||||
height = element.DesiredSize.Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Size(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
|
||||||
{
|
|
||||||
OnViewportChanged(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnParentMapPropertyChanged(MapBase parentMap)
|
|
||||||
{
|
|
||||||
if (ParentMap != null && ParentMap != this)
|
|
||||||
{
|
|
||||||
ParentMap.ViewportChanged -= OnViewportChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
ParentMap = parentMap;
|
|
||||||
|
|
||||||
if (ParentMap != null && ParentMap != this)
|
|
||||||
{
|
|
||||||
ParentMap.ViewportChanged += OnViewportChanged;
|
|
||||||
|
|
||||||
OnViewportChanged(new ViewportChangedEventArgs());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,231 +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 Avalonia.Media;
|
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MapControl
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Displays a standard Web Mercator map tile grid, e.g. an OpenStreetMap tile grid.
|
|
||||||
/// </summary>
|
|
||||||
public class MapTileLayer : MapTileLayerBase
|
|
||||||
{
|
|
||||||
private const int TileSize = 256;
|
|
||||||
|
|
||||||
private static readonly Point MapTopLeft = new(
|
|
||||||
-180d * MapProjection.Wgs84MeterPerDegree, 180d * MapProjection.Wgs84MeterPerDegree);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A default MapTileLayer using OpenStreetMap data.
|
|
||||||
/// </summary>
|
|
||||||
public static MapTileLayer OpenStreetMapTileLayer => new()
|
|
||||||
{
|
|
||||||
TileSource = new TileSource { UriTemplate = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" },
|
|
||||||
SourceName = "OpenStreetMap",
|
|
||||||
Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)"
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly StyledProperty<int> MinZoomLevelProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayer, int>(nameof(MinZoomLevel));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<int> MaxZoomLevelProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayer, int>(nameof(MaxZoomLevel), 19);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<double> ZoomLevelOffsetProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayer, double>(nameof(ZoomLevelOffset));
|
|
||||||
|
|
||||||
public TileMatrix TileMatrix { get; private set; }
|
|
||||||
|
|
||||||
public TileCollection Tiles { get; private set; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Minimum zoom level supported by the MapTileLayer. Default value is 0.
|
|
||||||
/// </summary>
|
|
||||||
public int MinZoomLevel
|
|
||||||
{
|
|
||||||
get => GetValue(MinZoomLevelProperty);
|
|
||||||
set => SetValue(MinZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum zoom level supported by the MapTileLayer. Default value is 19.
|
|
||||||
/// </summary>
|
|
||||||
public int MaxZoomLevel
|
|
||||||
{
|
|
||||||
get => GetValue(MaxZoomLevelProperty);
|
|
||||||
set => SetValue(MaxZoomLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional offset between the map zoom level and the topmost tile zoom level.
|
|
||||||
/// Default value is 0.
|
|
||||||
/// </summary>
|
|
||||||
public double ZoomLevelOffset
|
|
||||||
{
|
|
||||||
get => GetValue(ZoomLevelOffsetProperty);
|
|
||||||
set => SetValue(ZoomLevelOffsetProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Size MeasureOverride(Size availableSize)
|
|
||||||
{
|
|
||||||
availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
|
|
||||||
|
|
||||||
foreach (var tile in Tiles)
|
|
||||||
{
|
|
||||||
tile.Image.Measure(availableSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Size();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Size ArrangeOverride(Size finalSize)
|
|
||||||
{
|
|
||||||
if (TileMatrix != null)
|
|
||||||
{
|
|
||||||
foreach (var tile in Tiles)
|
|
||||||
{
|
|
||||||
// Arrange tiles relative to XMin/YMin.
|
|
||||||
//
|
|
||||||
var tileSize = TileSize << (TileMatrix.ZoomLevel - tile.ZoomLevel);
|
|
||||||
var x = tileSize * tile.X - TileSize * TileMatrix.XMin;
|
|
||||||
var y = tileSize * tile.Y - TileSize * TileMatrix.YMin;
|
|
||||||
|
|
||||||
tile.Image.Width = tileSize;
|
|
||||||
tile.Image.Height = tileSize;
|
|
||||||
tile.Image.Arrange(new Rect(x, y, tileSize, tileSize));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task UpdateTileLayer(bool tileSourceChanged)
|
|
||||||
{
|
|
||||||
var updateTiles = false;
|
|
||||||
|
|
||||||
if (ParentMap == null || ParentMap.MapProjection.Type != MapProjectionType.WebMercator)
|
|
||||||
{
|
|
||||||
updateTiles = TileMatrix != null;
|
|
||||||
TileMatrix = null;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (tileSourceChanged)
|
|
||||||
{
|
|
||||||
Tiles = []; // clear all
|
|
||||||
updateTiles = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SetTileMatrix())
|
|
||||||
{
|
|
||||||
updateTiles = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetRenderTransform();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateTiles)
|
|
||||||
{
|
|
||||||
UpdateTiles();
|
|
||||||
|
|
||||||
return LoadTiles(Tiles, SourceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void SetRenderTransform()
|
|
||||||
{
|
|
||||||
if (TileMatrix != null)
|
|
||||||
{
|
|
||||||
// Tile matrix origin in pixels.
|
|
||||||
//
|
|
||||||
var tileMatrixOrigin = new Point(TileSize * TileMatrix.XMin, TileSize * TileMatrix.YMin);
|
|
||||||
|
|
||||||
var tileMatrixScale = ViewTransform.ZoomLevelToScale(TileMatrix.ZoomLevel);
|
|
||||||
|
|
||||||
((MatrixTransform)RenderTransform).Matrix =
|
|
||||||
ParentMap.ViewTransform.GetTileLayerTransform(tileMatrixScale, MapTopLeft, tileMatrixOrigin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool SetTileMatrix()
|
|
||||||
{
|
|
||||||
// Add 0.001 to avoid rounding issues.
|
|
||||||
//
|
|
||||||
var tileMatrixZoomLevel = (int)Math.Floor(ParentMap.ZoomLevel - ZoomLevelOffset + 0.001);
|
|
||||||
|
|
||||||
var tileMatrixScale = ViewTransform.ZoomLevelToScale(tileMatrixZoomLevel);
|
|
||||||
|
|
||||||
// Bounds in tile pixels from view size.
|
|
||||||
//
|
|
||||||
var bounds = ParentMap.ViewTransform.GetTileMatrixBounds(tileMatrixScale, MapTopLeft, ParentMap.Bounds.Size);
|
|
||||||
|
|
||||||
// Tile X and Y bounds.
|
|
||||||
//
|
|
||||||
var xMin = (int)Math.Floor(bounds.X / TileSize);
|
|
||||||
var yMin = (int)Math.Floor(bounds.Y / TileSize);
|
|
||||||
var xMax = (int)Math.Floor((bounds.X + bounds.Width) / TileSize);
|
|
||||||
var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / TileSize);
|
|
||||||
|
|
||||||
if (TileMatrix != null &&
|
|
||||||
TileMatrix.ZoomLevel == tileMatrixZoomLevel &&
|
|
||||||
TileMatrix.XMin == xMin && TileMatrix.YMin == yMin &&
|
|
||||||
TileMatrix.XMax == xMax && TileMatrix.YMax == yMax)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
TileMatrix = new TileMatrix(tileMatrixZoomLevel, xMin, yMin, xMax, yMax);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateTiles()
|
|
||||||
{
|
|
||||||
var tiles = new TileCollection();
|
|
||||||
|
|
||||||
if (TileSource != null && TileMatrix != null)
|
|
||||||
{
|
|
||||||
var maxZoomLevel = Math.Min(TileMatrix.ZoomLevel, MaxZoomLevel);
|
|
||||||
|
|
||||||
if (maxZoomLevel >= MinZoomLevel)
|
|
||||||
{
|
|
||||||
var minZoomLevel = IsBaseMapLayer
|
|
||||||
? Math.Max(TileMatrix.ZoomLevel - MaxBackgroundLevels, MinZoomLevel)
|
|
||||||
: maxZoomLevel;
|
|
||||||
|
|
||||||
for (var z = minZoomLevel; z <= maxZoomLevel; z++)
|
|
||||||
{
|
|
||||||
var numTiles = 1 << z;
|
|
||||||
var tileSize = 1 << (TileMatrix.ZoomLevel - z);
|
|
||||||
var x1 = (int)Math.Floor((double)TileMatrix.XMin / tileSize); // may be negative
|
|
||||||
var x2 = TileMatrix.XMax / tileSize; // may be greater than numTiles-1
|
|
||||||
var y1 = Math.Max(TileMatrix.YMin / tileSize, 0);
|
|
||||||
var y2 = Math.Min(TileMatrix.YMax / tileSize, numTiles - 1);
|
|
||||||
|
|
||||||
for (var y = y1; y <= y2; y++)
|
|
||||||
{
|
|
||||||
for (var x = x1; x <= x2; x++)
|
|
||||||
{
|
|
||||||
tiles.Add(Tiles.GetTile(z, x, y, numTiles));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Tiles = tiles;
|
|
||||||
|
|
||||||
Children.Clear();
|
|
||||||
|
|
||||||
foreach (var tile in tiles)
|
|
||||||
{
|
|
||||||
Children.Add(tile.Image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,214 +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 Avalonia.Controls;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MapControl
|
|
||||||
{
|
|
||||||
public abstract class MapTileLayerBase : Panel
|
|
||||||
{
|
|
||||||
public static readonly StyledProperty<TileSource> TileSourceProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayerBase, TileSource>(nameof(TileSource));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<string> SourceNameProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayerBase, string>(nameof(SourceName));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<string> DescriptionProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayerBase, string>(nameof(Description));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<int> MaxBackgroundLevelsProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayerBase, int>(nameof(MaxBackgroundLevels), 5);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<TimeSpan> UpdateIntervalProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayerBase, TimeSpan>(nameof(AvaloniaProperty), TimeSpan.FromSeconds(0.2));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> UpdateWhileViewportChangingProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayerBase, bool>(nameof(UpdateWhileViewportChanging));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> MapBackgroundProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayerBase, IBrush>(nameof(MapBackground));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> MapForegroundProperty
|
|
||||||
= AvaloniaProperty.Register<MapTileLayerBase, IBrush>(nameof(MapForeground));
|
|
||||||
|
|
||||||
public static readonly DirectProperty<MapTileLayerBase, double> LoadingProgressProperty
|
|
||||||
= AvaloniaProperty.RegisterDirect<MapTileLayerBase, double>(nameof(LoadingProgress), layer => layer.loadingProgressValue);
|
|
||||||
|
|
||||||
private readonly DispatcherTimer updateTimer;
|
|
||||||
private readonly Progress<double> loadingProgress;
|
|
||||||
private double loadingProgressValue;
|
|
||||||
private ITileImageLoader tileImageLoader;
|
|
||||||
|
|
||||||
static MapTileLayerBase()
|
|
||||||
{
|
|
||||||
MapPanel.ParentMapProperty.Changed.AddClassHandler<MapTileLayerBase, MapBase>(
|
|
||||||
(layer, args) => layer.OnParentMapPropertyChanged(args.NewValue.Value));
|
|
||||||
|
|
||||||
TileSourceProperty.Changed.AddClassHandler<MapTileLayerBase, TileSource>(
|
|
||||||
async (layer, args) => await layer.Update(true));
|
|
||||||
|
|
||||||
UpdateIntervalProperty.Changed.AddClassHandler<MapTileLayerBase, TimeSpan>(
|
|
||||||
(layer, args) => layer.updateTimer.Interval = args.NewValue.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MapTileLayerBase()
|
|
||||||
{
|
|
||||||
RenderTransform = new MatrixTransform();
|
|
||||||
RenderTransformOrigin = new RelativePoint();
|
|
||||||
|
|
||||||
loadingProgress = new Progress<double>(p => SetAndRaise(LoadingProgressProperty, ref loadingProgressValue, p));
|
|
||||||
|
|
||||||
updateTimer = this.CreateTimer(UpdateInterval);
|
|
||||||
updateTimer.Tick += async (s, e) => await Update(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MapBase ParentMap { get; private set; }
|
|
||||||
|
|
||||||
public ITileImageLoader TileImageLoader
|
|
||||||
{
|
|
||||||
get => tileImageLoader ??= new TileImageLoader();
|
|
||||||
set => tileImageLoader = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides map tile URIs or images.
|
|
||||||
/// </summary>
|
|
||||||
public TileSource TileSource
|
|
||||||
{
|
|
||||||
get => GetValue(TileSourceProperty);
|
|
||||||
set => SetValue(TileSourceProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Name of the TileSource. Used as component of a tile cache key.
|
|
||||||
/// </summary>
|
|
||||||
public string SourceName
|
|
||||||
{
|
|
||||||
get => GetValue(SourceNameProperty);
|
|
||||||
set => SetValue(SourceNameProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Description of the layer. Used to display copyright information on top of the map.
|
|
||||||
/// </summary>
|
|
||||||
public string Description
|
|
||||||
{
|
|
||||||
get => GetValue(DescriptionProperty);
|
|
||||||
set => SetValue(DescriptionProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum number of background tile levels. Default value is 5.
|
|
||||||
/// Only effective in a MapTileLayer or WmtsTileLayer that is the MapLayer of its ParentMap.
|
|
||||||
/// </summary>
|
|
||||||
public int MaxBackgroundLevels
|
|
||||||
{
|
|
||||||
get => GetValue(MaxBackgroundLevelsProperty);
|
|
||||||
set => SetValue(MaxBackgroundLevelsProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Minimum time interval between tile updates.
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan UpdateInterval
|
|
||||||
{
|
|
||||||
get => GetValue(UpdateIntervalProperty);
|
|
||||||
set => SetValue(UpdateIntervalProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls if tiles are updated while the viewport is still changing.
|
|
||||||
/// </summary>
|
|
||||||
public bool UpdateWhileViewportChanging
|
|
||||||
{
|
|
||||||
get => GetValue(UpdateWhileViewportChangingProperty);
|
|
||||||
set => SetValue(UpdateWhileViewportChangingProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional background brush. Sets MapBase.Background if not null and this layer is the base map layer.
|
|
||||||
/// </summary>
|
|
||||||
public IBrush MapBackground
|
|
||||||
{
|
|
||||||
get => GetValue(MapBackgroundProperty);
|
|
||||||
set => SetValue(MapBackgroundProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional foreground brush. Sets MapBase.Foreground if not null and this layer is the base map layer.
|
|
||||||
/// </summary>
|
|
||||||
public IBrush MapForeground
|
|
||||||
{
|
|
||||||
get => GetValue(MapForegroundProperty);
|
|
||||||
set => SetValue(MapForegroundProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the progress of the TileImageLoader as a double value between 0 and 1.
|
|
||||||
/// </summary>
|
|
||||||
public double LoadingProgress => loadingProgressValue;
|
|
||||||
|
|
||||||
protected bool IsBaseMapLayer
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var parentMap = MapPanel.GetParentMap(this);
|
|
||||||
|
|
||||||
return parentMap != null && parentMap.Children.Count > 0 && parentMap.Children[0] == this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void SetRenderTransform();
|
|
||||||
|
|
||||||
protected abstract Task UpdateTileLayer(bool tileSourceChanged);
|
|
||||||
|
|
||||||
protected Task LoadTiles(IEnumerable<Tile> tiles, string cacheName)
|
|
||||||
{
|
|
||||||
return TileImageLoader.LoadTilesAsync(tiles, TileSource, cacheName, loadingProgress);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task Update(bool tileSourceChanged)
|
|
||||||
{
|
|
||||||
updateTimer.Stop();
|
|
||||||
|
|
||||||
return UpdateTileLayer(tileSourceChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnViewportChanged(object sender, ViewportChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.TransformCenterChanged || e.ProjectionChanged || Children.Count == 0)
|
|
||||||
{
|
|
||||||
await Update(false); // update immediately
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SetRenderTransform();
|
|
||||||
|
|
||||||
updateTimer.Run(!UpdateWhileViewportChanging);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnParentMapPropertyChanged(MapBase parentMap)
|
|
||||||
{
|
|
||||||
if (ParentMap != null)
|
|
||||||
{
|
|
||||||
ParentMap.ViewportChanged -= OnViewportChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
ParentMap = parentMap;
|
|
||||||
|
|
||||||
if (ParentMap != null)
|
|
||||||
{
|
|
||||||
ParentMap.ViewportChanged += OnViewportChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTimer.Run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +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 Avalonia.Threading;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MapControl
|
|
||||||
{
|
|
||||||
internal static class Timer
|
|
||||||
{
|
|
||||||
public static DispatcherTimer CreateTimer(this AvaloniaObject obj, TimeSpan interval)
|
|
||||||
{
|
|
||||||
var timer = new DispatcherTimer
|
|
||||||
{
|
|
||||||
Interval = interval
|
|
||||||
};
|
|
||||||
|
|
||||||
return timer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Run(this DispatcherTimer timer, bool restart = false)
|
|
||||||
{
|
|
||||||
if (restart)
|
|
||||||
{
|
|
||||||
timer.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!timer.IsEnabled)
|
|
||||||
{
|
|
||||||
timer.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
MapControl/Shared/DependencyPropertyHelper.cs
Normal file
71
MapControl/Shared/DependencyPropertyHelper.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
// 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;
|
||||||
|
#elif UWP
|
||||||
|
using Windows.UI.Xaml;
|
||||||
|
#else
|
||||||
|
using System.Windows;
|
||||||
|
#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> propertyChanged = 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)
|
||||||
|
{
|
||||||
|
metadata.PropertyChangedCallback = (o, e) => propertyChanged((TOwner)o, (TValue)e.OldValue, (TValue)e.NewValue);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
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> propertyChanged = 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)
|
||||||
|
{
|
||||||
|
metadata.PropertyChangedCallback = (o, e) => propertyChanged((FrameworkElement)o, (TValue)e.OldValue, (TValue)e.NewValue);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return DependencyProperty.RegisterAttached(name, typeof(TValue), typeof(TOwner), metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Copyright © 2024 Clemens Fischer
|
// Copyright © 2024 Clemens Fischer
|
||||||
// Licensed under the Microsoft Public License (Ms-PL)
|
// Licensed under the Microsoft Public License (Ms-PL)
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
|
|
@ -12,7 +11,7 @@ namespace MapControl
|
||||||
public static string GetFullPath(string path)
|
public static string GetFullPath(string path)
|
||||||
{
|
{
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
return Path.GetFullPath(path, AppDomain.CurrentDomain.BaseDirectory);
|
return Path.GetFullPath(path, System.AppDomain.CurrentDomain.BaseDirectory);
|
||||||
#else
|
#else
|
||||||
return Path.GetFullPath(path);
|
return Path.GetFullPath(path);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,10 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
#if WINUI
|
#if WINUI
|
||||||
using Windows.Foundation;
|
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Media;
|
|
||||||
using Microsoft.UI.Xaml.Media.Animation;
|
using Microsoft.UI.Xaml.Media.Animation;
|
||||||
#elif UWP
|
#elif UWP
|
||||||
using Windows.Foundation;
|
|
||||||
using Windows.UI.Xaml;
|
using Windows.UI.Xaml;
|
||||||
using Windows.UI.Xaml.Media;
|
|
||||||
using Windows.UI.Xaml.Media.Animation;
|
using Windows.UI.Xaml.Media.Animation;
|
||||||
#else
|
#else
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
@ -21,35 +17,8 @@ using System.Windows.Media.Animation;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
{
|
{
|
||||||
public interface IMapLayer : IMapElement
|
public partial class MapBase
|
||||||
{
|
{
|
||||||
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 MapLayerProperty = DependencyProperty.Register(
|
|
||||||
nameof(MapLayer), typeof(UIElement), typeof(MapBase),
|
|
||||||
new PropertyMetadata(null, (o, e) => ((MapBase)o).MapLayerPropertyChanged((UIElement)e.OldValue, (UIElement)e.NewValue)));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MapProjectionProperty = DependencyProperty.Register(
|
|
||||||
nameof(MapProjection), typeof(MapProjection), typeof(MapBase),
|
|
||||||
new PropertyMetadata(new WebMercatorProjection(), (o, e) => ((MapBase)o).MapProjectionPropertyChanged((MapProjection)e.NewValue)));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty ProjectionCenterProperty = DependencyProperty.Register(
|
|
||||||
nameof(ProjectionCenter), typeof(Location), typeof(MapBase),
|
|
||||||
new PropertyMetadata(null, (o, e) => ((MapBase)o).ProjectionCenterPropertyChanged()));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register(
|
||||||
nameof(MinZoomLevel), typeof(double), typeof(MapBase),
|
nameof(MinZoomLevel), typeof(double), typeof(MapBase),
|
||||||
new PropertyMetadata(1d, (o, e) => ((MapBase)o).MinZoomLevelPropertyChanged((double)e.NewValue)));
|
new PropertyMetadata(1d, (o, e) => ((MapBase)o).MinZoomLevelPropertyChanged((double)e.NewValue)));
|
||||||
|
|
@ -58,10 +27,6 @@ namespace MapControl
|
||||||
nameof(MaxZoomLevel), typeof(double), typeof(MapBase),
|
nameof(MaxZoomLevel), typeof(double), typeof(MapBase),
|
||||||
new PropertyMetadata(20d, (o, e) => ((MapBase)o).MaxZoomLevelPropertyChanged((double)e.NewValue)));
|
new PropertyMetadata(20d, (o, e) => ((MapBase)o).MaxZoomLevelPropertyChanged((double)e.NewValue)));
|
||||||
|
|
||||||
public static readonly DependencyProperty AnimationDurationProperty = DependencyProperty.Register(
|
|
||||||
nameof(AnimationDuration), typeof(TimeSpan), typeof(MapBase),
|
|
||||||
new PropertyMetadata(TimeSpan.FromSeconds(0.3)));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty AnimationEasingFunctionProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty AnimationEasingFunctionProperty = DependencyProperty.Register(
|
||||||
nameof(AnimationEasingFunction), typeof(EasingFunctionBase), typeof(MapBase),
|
nameof(AnimationEasingFunction), typeof(EasingFunctionBase), typeof(MapBase),
|
||||||
new PropertyMetadata(new QuadraticEase { EasingMode = EasingMode.EaseOut }));
|
new PropertyMetadata(new QuadraticEase { EasingMode = EasingMode.EaseOut }));
|
||||||
|
|
@ -69,139 +34,6 @@ namespace MapControl
|
||||||
private PointAnimation centerAnimation;
|
private PointAnimation centerAnimation;
|
||||||
private DoubleAnimation zoomLevelAnimation;
|
private DoubleAnimation zoomLevelAnimation;
|
||||||
private DoubleAnimation headingAnimation;
|
private DoubleAnimation headingAnimation;
|
||||||
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 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 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>
|
/// <summary>
|
||||||
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
|
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
|
||||||
|
|
@ -219,23 +51,6 @@ namespace MapControl
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double ViewScale => (double)GetValue(ViewScaleProperty);
|
public double ViewScale => (double)GetValue(ViewScaleProperty);
|
||||||
|
|
||||||
/// <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>
|
/// <summary>
|
||||||
/// Gets a transform Matrix for scaling and rotating objects that are anchored
|
/// Gets a transform Matrix for scaling and rotating objects that are anchored
|
||||||
/// at a Location from map coordinates (i.e. meters) to view coordinates.
|
/// at a Location from map coordinates (i.e. meters) to view coordinates.
|
||||||
|
|
@ -250,220 +65,6 @@ namespace MapControl
|
||||||
return transform;
|
return transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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 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)
|
private void MapProjectionPropertyChanged(MapProjection projection)
|
||||||
{
|
{
|
||||||
maxLatitude = 90d;
|
maxLatitude = 90d;
|
||||||
|
|
@ -484,12 +85,6 @@ namespace MapControl
|
||||||
UpdateTransform(false, true);
|
UpdateTransform(false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProjectionCenterPropertyChanged()
|
|
||||||
{
|
|
||||||
ResetTransformCenter();
|
|
||||||
UpdateTransform();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Location CoerceCenterProperty(DependencyProperty property, Location center)
|
private Location CoerceCenterProperty(DependencyProperty property, Location center)
|
||||||
{
|
{
|
||||||
var c = center;
|
var c = center;
|
||||||
|
|
@ -754,90 +349,5 @@ namespace MapControl
|
||||||
this.BeginAnimation(HeadingProperty, null);
|
this.BeginAnimation(HeadingProperty, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetValueInternal(DependencyProperty property, object value)
|
|
||||||
{
|
|
||||||
internalPropertyChange = true;
|
|
||||||
|
|
||||||
SetValue(property, value);
|
|
||||||
|
|
||||||
internalPropertyChange = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
519
MapControl/Shared/MapBaseCommon.cs
Normal file
519
MapControl/Shared/MapBaseCommon.cs
Normal file
|
|
@ -0,0 +1,519 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,14 @@
|
||||||
// Licensed under the Microsoft Public License (Ms-PL)
|
// Licensed under the Microsoft Public License (Ms-PL)
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
#if AVALONIA
|
||||||
#if WINUI
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using DependencyProperty = Avalonia.AvaloniaProperty;
|
||||||
|
using FrameworkElement = Avalonia.Controls.Control;
|
||||||
|
using HorizontalAlignment = Avalonia.Layout.HorizontalAlignment;
|
||||||
|
using VerticalAlignment = Avalonia.Layout.VerticalAlignment;
|
||||||
|
#elif WINUI
|
||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
@ -20,6 +26,10 @@ using System.Windows.Controls;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Arranges child elements on a Map at positions specified by the attached property Location,
|
||||||
|
/// or in rectangles specified by the attached property BoundingBox.
|
||||||
|
/// </summary>
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -30,19 +40,31 @@ namespace MapControl
|
||||||
MapBase ParentMap { get; set; }
|
MapBase ParentMap { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Arranges child elements on a Map at positions specified by the attached property Location,
|
|
||||||
/// or in rectangles specified by the attached property BoundingBox.
|
|
||||||
/// </summary>
|
|
||||||
public partial class MapPanel : Panel, IMapElement
|
public partial class MapPanel : Panel, IMapElement
|
||||||
{
|
{
|
||||||
private static void ParentMapPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
|
public static readonly DependencyProperty AutoCollapseProperty =
|
||||||
{
|
DependencyPropertyHelper.RegisterAttached<MapPanel, bool>("AutoCollapse");
|
||||||
if (obj is IMapElement mapElement)
|
|
||||||
{
|
public static readonly DependencyProperty LocationProperty =
|
||||||
mapElement.ParentMap = e.NewValue as MapBase;
|
DependencyPropertyHelper.RegisterAttached<MapPanel, Location>("Location", null, false,
|
||||||
}
|
(obj, oldVale, newValue) => (obj.Parent as MapPanel)?.InvalidateArrange());
|
||||||
}
|
|
||||||
|
public static readonly DependencyProperty BoundingBoxProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<MapPanel, BoundingBox>("BoundingBox", null, false,
|
||||||
|
(obj, oldVale, newValue) => (obj.Parent as MapPanel)?.InvalidateArrange());
|
||||||
|
|
||||||
|
private static readonly DependencyProperty ViewPositionProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<MapPanel, Point?>("ViewPosition");
|
||||||
|
|
||||||
|
private static readonly DependencyProperty ParentMapProperty =
|
||||||
|
DependencyPropertyHelper.RegisterAttached<MapPanel, MapBase>("ParentMap", null, true,
|
||||||
|
(obj, oldVale, newValue) =>
|
||||||
|
{
|
||||||
|
if (obj is IMapElement mapElement)
|
||||||
|
{
|
||||||
|
mapElement.ParentMap = newValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
private MapBase parentMap;
|
private MapBase parentMap;
|
||||||
|
|
||||||
|
|
@ -55,9 +77,6 @@ namespace MapControl
|
||||||
set => SetParentMap(value);
|
set => SetParentMap(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty AutoCollapseProperty = DependencyProperty.RegisterAttached(
|
|
||||||
"AutoCollapse", typeof(bool), typeof(MapPanel), new PropertyMetadata(false));
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value that controls whether an element's Visibility is automatically
|
/// Gets a value that controls whether an element's Visibility is automatically
|
||||||
/// set to Collapsed when it is located outside the visible viewport area.
|
/// set to Collapsed when it is located outside the visible viewport area.
|
||||||
|
|
@ -115,6 +134,16 @@ namespace MapControl
|
||||||
return (Point?)element.GetValue(ViewPositionProperty);
|
return (Point?)element.GetValue(ViewPositionProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the attached ViewPosition property of an element. The method is called during
|
||||||
|
/// ArrangeOverride and may be overridden to modify the actual view position value.
|
||||||
|
/// An overridden method should call this method to set the attached property.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void SetViewPosition(FrameworkElement element, ref Point? position)
|
||||||
|
{
|
||||||
|
element.SetValue(ViewPositionProperty, position);
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void SetParentMap(MapBase map)
|
protected virtual void SetParentMap(MapBase map)
|
||||||
{
|
{
|
||||||
if (parentMap != null && parentMap != this)
|
if (parentMap != null && parentMap != this)
|
||||||
|
|
@ -146,7 +175,7 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
|
availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
|
||||||
|
|
||||||
foreach (var element in Children.OfType<FrameworkElement>())
|
foreach (var element in ChildElements)
|
||||||
{
|
{
|
||||||
element.Measure(availableSize);
|
element.Measure(availableSize);
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +187,7 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
if (parentMap != null)
|
if (parentMap != null)
|
||||||
{
|
{
|
||||||
foreach (var element in Children.OfType<FrameworkElement>())
|
foreach (var element in ChildElements)
|
||||||
{
|
{
|
||||||
var location = GetLocation(element);
|
var location = GetLocation(element);
|
||||||
var position = location != null ? GetViewPosition(location) : null;
|
var position = location != null ? GetViewPosition(location) : null;
|
||||||
|
|
@ -167,8 +196,7 @@ namespace MapControl
|
||||||
|
|
||||||
if (GetAutoCollapse(element))
|
if (GetAutoCollapse(element))
|
||||||
{
|
{
|
||||||
element.Visibility = position.HasValue && IsOutsideViewport(position.Value)
|
SetVisible(element, !(position.HasValue && IsOutsideViewport(position.Value)));
|
||||||
? Visibility.Collapsed : Visibility.Visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (position.HasValue)
|
if (position.HasValue)
|
||||||
|
|
@ -230,11 +258,11 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
var rectCenter = new Point(rect.X + rect.Width / 2d, rect.Y + rect.Height / 2d);
|
var rectCenter = new Point(rect.X + rect.Width / 2d, rect.Y + rect.Height / 2d);
|
||||||
var position = parentMap.ViewTransform.MapToView(rectCenter);
|
var position = parentMap.ViewTransform.MapToView(rectCenter);
|
||||||
|
var projection = parentMap.MapProjection;
|
||||||
|
|
||||||
if (parentMap.MapProjection.Type <= MapProjectionType.NormalCylindrical &&
|
if (projection.Type <= MapProjectionType.NormalCylindrical && IsOutsideViewport(position))
|
||||||
IsOutsideViewport(position))
|
|
||||||
{
|
{
|
||||||
var location = parentMap.MapProjection.MapToLocation(rectCenter);
|
var location = projection.MapToLocation(rectCenter);
|
||||||
|
|
||||||
if (location != null)
|
if (location != null)
|
||||||
{
|
{
|
||||||
|
|
@ -359,9 +387,7 @@ namespace MapControl
|
||||||
}
|
}
|
||||||
else if (rect.Rotation != 0d)
|
else if (rect.Rotation != 0d)
|
||||||
{
|
{
|
||||||
rotateTransform = new RotateTransform { Angle = rect.Rotation };
|
SetRenderTransform(element, new RotateTransform { Angle = rect.Rotation }, 0.5, 0.5);
|
||||||
element.RenderTransform = rotateTransform;
|
|
||||||
element.RenderTransformOrigin = new Point(0.5, 0.5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,7 +401,7 @@ namespace MapControl
|
||||||
element.Arrange(rect);
|
element.Arrange(rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Size GetDesiredSize(UIElement element)
|
internal static Size GetDesiredSize(FrameworkElement element)
|
||||||
{
|
{
|
||||||
var width = 0d;
|
var width = 0d;
|
||||||
var height = 0d;
|
var height = 0d;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
#if WINUI
|
#if AVALONIA
|
||||||
|
using Avalonia.Media;
|
||||||
|
using DependencyProperty = Avalonia.AvaloniaProperty;
|
||||||
|
#elif WINUI
|
||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
|
@ -24,6 +27,15 @@ namespace MapControl
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MapTileLayer : MapTileLayerBase
|
public class MapTileLayer : MapTileLayerBase
|
||||||
{
|
{
|
||||||
|
public static readonly DependencyProperty MinZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapTileLayer, int>(nameof(MinZoomLevel), 0);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MaxZoomLevelProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapTileLayer, int>(nameof(MaxZoomLevel), 19);
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ZoomLevelOffsetProperty =
|
||||||
|
DependencyPropertyHelper.Register<MapTileLayer, double>(nameof(ZoomLevelOffset), 0d);
|
||||||
|
|
||||||
private const int TileSize = 256;
|
private const int TileSize = 256;
|
||||||
|
|
||||||
private static readonly Point MapTopLeft = new Point(
|
private static readonly Point MapTopLeft = new Point(
|
||||||
|
|
@ -39,15 +51,6 @@ namespace MapControl
|
||||||
Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)"
|
Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)"
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register(
|
|
||||||
nameof(MinZoomLevel), typeof(int), typeof(MapTileLayer), new PropertyMetadata(0));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty MaxZoomLevelProperty = DependencyProperty.Register(
|
|
||||||
nameof(MaxZoomLevel), typeof(int), typeof(MapTileLayer), new PropertyMetadata(19));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty ZoomLevelOffsetProperty = DependencyProperty.Register(
|
|
||||||
nameof(ZoomLevelOffset), typeof(double), typeof(MapTileLayer), new PropertyMetadata(0d));
|
|
||||||
|
|
||||||
public TileMatrix TileMatrix { get; private set; }
|
public TileMatrix TileMatrix { get; private set; }
|
||||||
|
|
||||||
public TileCollection Tiles { get; private set; } = new TileCollection();
|
public TileCollection Tiles { get; private set; } = new TileCollection();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
#if WINUI
|
#if AVALONIA
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using DependencyProperty = Avalonia.AvaloniaProperty;
|
||||||
|
#elif WINUI
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
|
@ -23,36 +28,35 @@ using System.Windows.Threading;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
{
|
{
|
||||||
public abstract class MapTileLayerBase : Panel, IMapLayer
|
public abstract partial class MapTileLayerBase : Panel, IMapLayer
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty TileSourceProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty TileSourceProperty =
|
||||||
nameof(TileSource), typeof(TileSource), typeof(MapTileLayerBase),
|
DependencyPropertyHelper.Register<MapTileLayerBase, TileSource>(nameof(TileSource), null, false,
|
||||||
new PropertyMetadata(null, async (o, e) => await ((MapTileLayerBase)o).Update(true)));
|
async (obj, oldVale, newValue) => await obj.Update(true));
|
||||||
|
|
||||||
public static readonly DependencyProperty SourceNameProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty SourceNameProperty =
|
||||||
nameof(SourceName), typeof(string), typeof(MapTileLayerBase), new PropertyMetadata(null));
|
DependencyPropertyHelper.Register<MapTileLayerBase, string>(nameof(SourceName));
|
||||||
|
|
||||||
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty DescriptionProperty =
|
||||||
nameof(Description), typeof(string), typeof(MapTileLayerBase), new PropertyMetadata(null));
|
DependencyPropertyHelper.Register<MapTileLayerBase, string>(nameof(Description));
|
||||||
|
|
||||||
public static readonly DependencyProperty MaxBackgroundLevelsProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty MaxBackgroundLevelsProperty =
|
||||||
nameof(MaxBackgroundLevels), typeof(int), typeof(MapTileLayerBase), new PropertyMetadata(5));
|
DependencyPropertyHelper.Register<MapTileLayerBase, int>(nameof(MaxBackgroundLevels), 5);
|
||||||
|
|
||||||
public static readonly DependencyProperty UpdateIntervalProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty UpdateIntervalProperty =
|
||||||
nameof(UpdateInterval), typeof(TimeSpan), typeof(MapTileLayerBase),
|
DependencyPropertyHelper.Register<MapTileLayerBase, TimeSpan>(nameof(UpdateInterval), TimeSpan.FromSeconds(0.2));
|
||||||
new PropertyMetadata(TimeSpan.FromSeconds(0.2), (o, e) => ((MapTileLayerBase)o).updateTimer.Interval = (TimeSpan)e.NewValue));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty UpdateWhileViewportChangingProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty UpdateWhileViewportChangingProperty =
|
||||||
nameof(UpdateWhileViewportChanging), typeof(bool), typeof(MapTileLayerBase), new PropertyMetadata(false));
|
DependencyPropertyHelper.Register<MapTileLayerBase, bool>(nameof(UpdateWhileViewportChanging));
|
||||||
|
|
||||||
public static readonly DependencyProperty MapBackgroundProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty MapBackgroundProperty =
|
||||||
nameof(MapBackground), typeof(Brush), typeof(MapTileLayerBase), new PropertyMetadata(null));
|
DependencyPropertyHelper.Register<MapTileLayerBase, Brush>(nameof(MapBackground));
|
||||||
|
|
||||||
public static readonly DependencyProperty MapForegroundProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty MapForegroundProperty =
|
||||||
nameof(MapForeground), typeof(Brush), typeof(MapTileLayerBase), new PropertyMetadata(null));
|
DependencyPropertyHelper.Register<MapTileLayerBase, Brush>(nameof(MapForeground));
|
||||||
|
|
||||||
public static readonly DependencyProperty LoadingProgressProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty LoadingProgressProperty =
|
||||||
nameof(LoadingProgress), typeof(double), typeof(MapTileLayerBase), new PropertyMetadata(1d));
|
DependencyPropertyHelper.Register<MapTileLayerBase, double>(nameof(LoadingProgress), 1d);
|
||||||
|
|
||||||
private readonly Progress<double> loadingProgress;
|
private readonly Progress<double> loadingProgress;
|
||||||
private readonly DispatcherTimer updateTimer;
|
private readonly DispatcherTimer updateTimer;
|
||||||
|
|
@ -61,9 +65,9 @@ namespace MapControl
|
||||||
|
|
||||||
protected MapTileLayerBase()
|
protected MapTileLayerBase()
|
||||||
{
|
{
|
||||||
RenderTransform = new MatrixTransform();
|
MapPanel.SetRenderTransform(this, new MatrixTransform());
|
||||||
|
|
||||||
loadingProgress = new Progress<double>(p => LoadingProgress = p);
|
loadingProgress = new Progress<double>(p => SetValue(LoadingProgressProperty, p));
|
||||||
|
|
||||||
updateTimer = this.CreateTimer(UpdateInterval);
|
updateTimer = this.CreateTimer(UpdateInterval);
|
||||||
updateTimer.Tick += async (s, e) => await Update(false);
|
updateTimer.Tick += async (s, e) => await Update(false);
|
||||||
|
|
@ -155,11 +159,7 @@ namespace MapControl
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the progress of the TileImageLoader as a double value between 0 and 1.
|
/// Gets the progress of the TileImageLoader as a double value between 0 and 1.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double LoadingProgress
|
public double LoadingProgress => (double)GetValue(LoadingProgressProperty);
|
||||||
{
|
|
||||||
get => (double)GetValue(LoadingProgressProperty);
|
|
||||||
private set => SetValue(LoadingProgressProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements IMapElement.ParentMap.
|
/// Implements IMapElement.ParentMap.
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,9 @@
|
||||||
<Compile Include="..\Shared\CenteredBoundingBox.cs">
|
<Compile Include="..\Shared\CenteredBoundingBox.cs">
|
||||||
<Link>CenteredBoundingBox.cs</Link>
|
<Link>CenteredBoundingBox.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="..\Shared\DependencyPropertyHelper.cs">
|
||||||
|
<Link>DependencyPropertyHelper.cs</Link>
|
||||||
|
</Compile>
|
||||||
<Compile Include="..\Shared\EquirectangularProjection.cs">
|
<Compile Include="..\Shared\EquirectangularProjection.cs">
|
||||||
<Link>EquirectangularProjection.cs</Link>
|
<Link>EquirectangularProjection.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
|
@ -104,6 +107,9 @@
|
||||||
<Compile Include="..\Shared\MapBase.cs">
|
<Compile Include="..\Shared\MapBase.cs">
|
||||||
<Link>MapBase.cs</Link>
|
<Link>MapBase.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="..\Shared\MapBaseCommon.cs">
|
||||||
|
<Link>MapBaseCommon.cs</Link>
|
||||||
|
</Compile>
|
||||||
<Compile Include="..\Shared\MapBorderPanel.cs">
|
<Compile Include="..\Shared\MapBorderPanel.cs">
|
||||||
<Link>MapBorderPanel.cs</Link>
|
<Link>MapBorderPanel.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,11 @@
|
||||||
// Licensed under the Microsoft Public License (Ms-PL)
|
// Licensed under the Microsoft Public License (Ms-PL)
|
||||||
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
{
|
{
|
||||||
public partial class MapBase
|
public partial class MapBase
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty ForegroundProperty =
|
|
||||||
Control.ForegroundProperty.AddOwner(typeof(MapBase));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty CenterProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty CenterProperty = DependencyProperty.Register(
|
||||||
nameof(Center), typeof(Location), typeof(MapBase), new FrameworkPropertyMetadata(
|
nameof(Center), typeof(Location), typeof(MapBase), new FrameworkPropertyMetadata(
|
||||||
new Location(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
new Location(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,20 @@
|
||||||
// Copyright © 2024 Clemens Fischer
|
// Copyright © 2024 Clemens Fischer
|
||||||
// Licensed under the Microsoft Public License (Ms-PL)
|
// Licensed under the Microsoft Public License (Ms-PL)
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
namespace MapControl
|
namespace MapControl
|
||||||
{
|
{
|
||||||
public partial class MapPanel
|
public partial class MapPanel
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty LocationProperty = DependencyProperty.RegisterAttached(
|
|
||||||
"Location", typeof(Location), typeof(MapPanel),
|
|
||||||
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsParentArrange));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty BoundingBoxProperty = DependencyProperty.RegisterAttached(
|
|
||||||
"BoundingBox", typeof(BoundingBox), typeof(MapPanel),
|
|
||||||
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsParentArrange));
|
|
||||||
|
|
||||||
private static readonly DependencyPropertyKey ParentMapPropertyKey = DependencyProperty.RegisterAttachedReadOnly(
|
|
||||||
"ParentMap", typeof(MapBase), typeof(MapPanel),
|
|
||||||
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, ParentMapPropertyChanged));
|
|
||||||
|
|
||||||
private static readonly DependencyPropertyKey ViewPositionPropertyKey = DependencyProperty.RegisterAttachedReadOnly(
|
|
||||||
"ViewPosition", typeof(Point?), typeof(MapPanel), new PropertyMetadata());
|
|
||||||
|
|
||||||
public static readonly DependencyProperty ParentMapProperty = ParentMapPropertyKey.DependencyProperty;
|
|
||||||
public static readonly DependencyProperty ViewPositionProperty = ViewPositionPropertyKey.DependencyProperty;
|
|
||||||
|
|
||||||
public MapPanel()
|
public MapPanel()
|
||||||
{
|
{
|
||||||
if (this is MapBase)
|
if (this is MapBase)
|
||||||
{
|
{
|
||||||
SetValue(ParentMapPropertyKey, this);
|
SetValue(ParentMapProperty, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,14 +24,17 @@ namespace MapControl
|
||||||
return (MapBase)element.GetValue(ParentMapProperty);
|
return (MapBase)element.GetValue(ParentMapProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static void SetRenderTransform(FrameworkElement element, Transform transform, double originX = 0d, double originY = 0d)
|
||||||
/// Sets the attached ViewPosition property of an element. The method is called during
|
|
||||||
/// ArrangeOverride and may be overridden to modify the actual view position value.
|
|
||||||
/// An overridden method should call this method to set the attached property.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void SetViewPosition(FrameworkElement element, ref Point? position)
|
|
||||||
{
|
{
|
||||||
element.SetValue(ViewPositionPropertyKey, position);
|
element.RenderTransform = transform;
|
||||||
|
element.RenderTransformOrigin = new Point(originX, originY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<FrameworkElement> ChildElements => Children.OfType<FrameworkElement>();
|
||||||
|
|
||||||
|
private static void SetVisible(FrameworkElement element, bool visible)
|
||||||
|
{
|
||||||
|
element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@
|
||||||
// Licensed under the Microsoft Public License (Ms-PL)
|
// Licensed under the Microsoft Public License (Ms-PL)
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
#if UWP
|
#if AVALONIA
|
||||||
|
using Avalonia.Threading;
|
||||||
|
#elif UWP
|
||||||
using Windows.UI.Xaml;
|
using Windows.UI.Xaml;
|
||||||
#else
|
#else
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
@ -14,14 +15,9 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
internal static class Timer
|
internal static class Timer
|
||||||
{
|
{
|
||||||
public static DispatcherTimer CreateTimer(this DependencyObject obj, TimeSpan interval)
|
public static DispatcherTimer CreateTimer(this object _, TimeSpan interval)
|
||||||
{
|
{
|
||||||
var timer = new DispatcherTimer
|
return new DispatcherTimer { Interval = interval };
|
||||||
{
|
|
||||||
Interval = interval
|
|
||||||
};
|
|
||||||
|
|
||||||
return timer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Run(this DispatcherTimer timer, bool restart = false)
|
public static void Run(this DispatcherTimer timer, bool restart = false)
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,6 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
public partial class MapBase
|
public partial class MapBase
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register(
|
|
||||||
nameof(Foreground), typeof(Brush), typeof(MapBase), new PropertyMetadata(new SolidColorBrush(Colors.Black)));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty CenterProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty CenterProperty = DependencyProperty.Register(
|
||||||
nameof(Center), typeof(Location), typeof(MapBase),
|
nameof(Center), typeof(Location), typeof(MapBase),
|
||||||
new PropertyMetadata(new Location(), (o, e) => ((MapBase)o).CenterPropertyChanged((Location)e.NewValue)));
|
new PropertyMetadata(new Location(), (o, e) => ((MapBase)o).CenterPropertyChanged((Location)e.NewValue)));
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
// Copyright © 2024 Clemens Fischer
|
// Copyright © 2024 Clemens Fischer
|
||||||
// Licensed under the Microsoft Public License (Ms-PL)
|
// Licensed under the Microsoft Public License (Ms-PL)
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
#if WINUI
|
#if WINUI
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
|
@ -14,20 +16,6 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
public partial class MapPanel
|
public partial class MapPanel
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty LocationProperty = DependencyProperty.RegisterAttached(
|
|
||||||
"Location", typeof(Location), typeof(MapPanel),
|
|
||||||
new PropertyMetadata(null, (o, e) => (((FrameworkElement)o).Parent as MapPanel)?.InvalidateArrange()));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty BoundingBoxProperty = DependencyProperty.RegisterAttached(
|
|
||||||
"BoundingBox", typeof(BoundingBox), typeof(MapPanel),
|
|
||||||
new PropertyMetadata(null, (o, e) => (((FrameworkElement)o).Parent as MapPanel)?.InvalidateArrange()));
|
|
||||||
|
|
||||||
public static readonly DependencyProperty ParentMapProperty = DependencyProperty.RegisterAttached(
|
|
||||||
"ParentMap", typeof(MapBase), typeof(MapPanel), new PropertyMetadata(null, ParentMapPropertyChanged));
|
|
||||||
|
|
||||||
private static readonly DependencyProperty ViewPositionProperty = DependencyProperty.RegisterAttached(
|
|
||||||
"ViewPosition", typeof(Point?), typeof(MapPanel), new PropertyMetadata(null));
|
|
||||||
|
|
||||||
public MapPanel()
|
public MapPanel()
|
||||||
{
|
{
|
||||||
InitMapElement(this);
|
InitMapElement(this);
|
||||||
|
|
@ -53,6 +41,8 @@ namespace MapControl
|
||||||
{
|
{
|
||||||
var parentMap = (MapBase)element.GetValue(ParentMapProperty);
|
var parentMap = (MapBase)element.GetValue(ParentMapProperty);
|
||||||
|
|
||||||
|
// Traverse visual tree because of missing property value inheritance.
|
||||||
|
|
||||||
if (parentMap == null &&
|
if (parentMap == null &&
|
||||||
VisualTreeHelper.GetParent(element) is FrameworkElement parentElement)
|
VisualTreeHelper.GetParent(element) is FrameworkElement parentElement)
|
||||||
{
|
{
|
||||||
|
|
@ -67,14 +57,17 @@ namespace MapControl
|
||||||
return parentMap;
|
return parentMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static void SetRenderTransform(FrameworkElement element, Transform transform, double originX = 0d, double originY = 0d)
|
||||||
/// Sets the attached ViewPosition property of an element. The method is called during
|
|
||||||
/// ArrangeOverride and may be overridden to modify the actual view position value.
|
|
||||||
/// An overridden method should call this method to set the attached property.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void SetViewPosition(FrameworkElement element, ref Point? position)
|
|
||||||
{
|
{
|
||||||
element.SetValue(ViewPositionProperty, position);
|
element.RenderTransform = transform;
|
||||||
|
element.RenderTransformOrigin = new Point(originX, originY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<FrameworkElement> ChildElements => Children.OfType<FrameworkElement>();
|
||||||
|
|
||||||
|
private static void SetVisible(FrameworkElement element, bool visible)
|
||||||
|
{
|
||||||
|
element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue