Initial Avalonia version

This commit is contained in:
ClemensFischer 2024-05-19 23:23:27 +02:00
parent 76b35d00a7
commit 66020793f8
11 changed files with 1750 additions and 0 deletions

View file

@ -0,0 +1,45 @@
// 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 Avalonia.Media.Imaging;
using System;
using System.IO;
using System.Threading.Tasks;
namespace MapControl
{
public static partial class ImageLoader
{
public static IImage LoadImage(Uri uri)
{
return null;
}
public static IImage LoadImage(Stream stream)
{
return new Bitmap(stream);
}
public static Task<IImage> LoadImageAsync(Stream stream)
{
return Task.FromResult(LoadImage(stream));
}
public static Task<IImage> LoadImageAsync(string path)
{
return Task.Run(() =>
{
if (!File.Exists(path))
{
return null;
}
using var stream = File.OpenRead(path);
return LoadImage(stream);
});
}
}
}

View file

@ -0,0 +1,96 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// Copyright © 2024 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using Avalonia.Input;
using System;
using System.Threading;
namespace MapControl
{
/// <summary>
/// MapBase with default input event handling.
/// </summary>
public class Map : MapBase
{
public static readonly StyledProperty<double> MouseWheelZoomDeltaProperty
= AvaloniaProperty.Register<Map, double>(nameof(MouseWheelZoomDelta), 0.25);
private Point? mousePosition;
private double targetZoomLevel;
private CancellationTokenSource cancellationTokenSource;
public Map()
{
PointerWheelChanged += OnPointerWheelChanged;
}
/// <summary>
/// Gets or sets the amount by which the ZoomLevel property changes by a MouseWheel event.
/// The default value is 0.25.
/// </summary>
public double MouseWheelZoomDelta
{
get => GetValue(MouseWheelZoomDeltaProperty);
set => SetValue(MouseWheelZoomDeltaProperty, value);
}
private async void OnPointerWheelChanged(object sender, PointerWheelEventArgs e)
{
var delta = MouseWheelZoomDelta * e.Delta.Y;
if (cancellationTokenSource != null)
{
cancellationTokenSource?.Cancel();
targetZoomLevel += delta;
}
else
{
targetZoomLevel = ZoomLevel + delta;
}
cancellationTokenSource = new CancellationTokenSource();
await ZoomMap(e.GetPosition(this), targetZoomLevel, cancellationTokenSource.Token);
cancellationTokenSource.Dispose();
cancellationTokenSource = null;
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
var point = e.GetCurrentPoint(this);
if (point.Properties.IsLeftButtonPressed)
{
e.Pointer.Capture(this);
mousePosition = point.Position;
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (mousePosition.HasValue)
{
e.Pointer.Capture(null);
mousePosition = null;
}
}
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (mousePosition.HasValue)
{
var position = e.GetPosition(this);
TranslateMap(position - mousePosition.Value);
mousePosition = position;
}
}
}
}

View file

@ -0,0 +1,564 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// Copyright © 2024 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
global using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Styling;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace MapControl
{
public interface IMapLayer
{
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
= AvaloniaProperty.Register<MapBase, Location>(nameof(Center), new Location(), false,
BindingMode.TwoWay, null, (map, center) => ((MapBase)map).CoerceCenterProperty(center));
public static readonly StyledProperty<double> MinZoomLevelProperty
= AvaloniaProperty.Register<MapBase, double>(nameof(MinZoomLevel), 1d, false,
BindingMode.OneWay, null, (map, minZoomLevel) => ((MapBase)map).CoerceMinZoomLevelProperty(minZoomLevel));
public static readonly StyledProperty<double> MaxZoomLevelProperty
= AvaloniaProperty.Register<MapBase, double>(nameof(MaxZoomLevel), 20d, false,
BindingMode.OneWay, null, (map, maxZoomLevel) => ((MapBase)map).CoerceMaxZoomLevelProperty(maxZoomLevel));
public static readonly StyledProperty<double> ZoomLevelProperty
= AvaloniaProperty.Register<MapBase, double>(nameof(ZoomLevel), 1d, false,
BindingMode.TwoWay, null, (map, zoomLevel) => ((MapBase)map).CoerceZoomLevelProperty(zoomLevel));
public static readonly StyledProperty<double> HeadingProperty
= AvaloniaProperty.Register<MapBase, double>(nameof(Heading), 0d, false,
BindingMode.TwoWay, null, (map, heading) => ((heading % 360d) + 360d) % 360d);
public static readonly DirectProperty<MapBase, double> ViewScaleProperty
= AvaloniaProperty.RegisterDirect<MapBase, double>(nameof(ViewScale), map => map.ViewScale);
private Location transformCenter;
private Point viewCenter;
private double centerLongitude;
private double maxLatitude = 90d;
static MapBase()
{
MapLayerProperty.Changed.AddClassHandler<MapBase, Control>(
(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>(
(map, args) => map.UpdateTransform());
ZoomLevelProperty.Changed.AddClassHandler<MapBase, double>(
(map, args) => map.UpdateTransform());
HeadingProperty.Changed.AddClassHandler<MapBase, double>(
(map, args) => map.UpdateTransform());
}
public MapBase()
{
MapProjectionPropertyChanged(MapProjection);
}
/// <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 IBrush Foreground
{
get => 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 => 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>
/// Gets the scaling factor from projected map coordinates to view coordinates,
/// as pixels per meter.
/// </summary>
public double ViewScale
{
get => ViewTransform.Scale;
}
/// <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)
{
return MapProjection.GetRelativeScale(location) * ViewTransform.Scale;
}
/// <summary>
/// Gets a transform Matrix for scaling and rotating objects that are anchored
/// at a Location from map coordinates (i.e. meters) to view coordinates.
/// </summary>
public Matrix GetMapTransform(Location location)
{
var scale = GetScale(location);
return new Matrix(scale.X, 0d, 0d, scale.Y, 0d, 0d)
.Append(Matrix.CreateRotation(ViewTransform.Rotation));
}
/// <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)
{
maxLatitude = 90d;
if (projection.Type <= MapProjectionType.NormalCylindrical)
{
var maxLocation = projection.MapToLocation(new Point(0d, 180d * MapProjection.Wgs84MeterPerDegree));
if (maxLocation != null && maxLocation.Latitude < 90d)
{
maxLatitude = maxLocation.Latitude;
Center = CoerceCenterProperty(Center);
}
}
ResetTransformCenter();
UpdateTransform(false, true);
}
private void ProjectionCenterPropertyChanged()
{
ResetTransformCenter();
UpdateTransform();
}
private Location CoerceCenterProperty(Location center)
{
if (center == null)
{
center = new Location();
}
else if (
center.Latitude < -maxLatitude || center.Latitude > maxLatitude ||
center.Longitude < -180d || center.Longitude > 180d)
{
center = new Location(
Math.Min(Math.Max(center.Latitude, -maxLatitude), maxLatitude),
Location.NormalizeLongitude(center.Longitude));
}
return center;
}
private double CoerceMinZoomLevelProperty(double minZoomLevel)
{
return Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel);
}
private double CoerceMaxZoomLevelProperty(double maxZoomLevel)
{
return Math.Max(maxZoomLevel, MinZoomLevel);
}
private double CoerceZoomLevelProperty(double zoomLevel)
{
return Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
}
}
}

View file

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<RootNamespace>MapControl</RootNamespace>
<AssemblyTitle>XAML Map Control Library for Avalonia UI</AssemblyTitle>
<Product>XAML Map Control</Product>
<Version>10.0.0</Version>
<Authors>Clemens Fischer</Authors>
<Copyright>Copyright © 2024 Clemens Fischer</Copyright>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\MapControl.snk</AssemblyOriginatorKeyFile>
<DelaySign>false</DelaySign>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<PackageId>XAML.MapControl</PackageId>
<DefineConstants>AVALONIA</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Shared\BoundingBox.cs" Link="BoundingBox.cs" />
<Compile Include="..\Shared\ImageFileCache.cs" Link="ImageFileCache.cs" />
<Compile Include="..\Shared\ImageLoader.cs" Link="ImageLoader.cs" />
<Compile Include="..\Shared\Location.cs" Link="Location.cs" />
<Compile Include="..\Shared\LocationCollection.cs" Link="LocationCollection.cs" />
<Compile Include="..\Shared\MapProjection.cs" Link="MapProjection.cs" />
<Compile Include="..\Shared\Tile.cs" Link="Tile.cs" />
<Compile Include="..\Shared\TileCollection.cs" Link="TileCollection.cs" />
<Compile Include="..\Shared\TileImageLoader.cs" Link="TileImageLoader.cs" />
<Compile Include="..\Shared\TileMatrix.cs" Link="TileMatrix.cs" />
<Compile Include="..\Shared\TileSource.cs" Link="TileSource.cs" />
<Compile Include="..\Shared\ViewportChangedEventArgs.cs" Link="ViewportChangedEventArgs.cs" />
<Compile Include="..\Shared\ViewRect.cs" Link="ViewRect.cs" />
<Compile Include="..\Shared\WebMercatorProjection.cs" Link="WebMercatorProjection.cs" />
<Compile Include="..\WPF\TypeConverters.cs" Link="TypeConverters.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.10" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,344 @@
// 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 System;
namespace MapControl
{
public class MapPanel : Panel
{
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()
{
ClipToBounds = true;
if (this is MapBase mapBase)
{
SetParentMap(this, mapBase);
}
}
public MapBase ParentMap { get; private set; }
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();
}
protected override Size MeasureOverride(Size availableSize)
{
availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
foreach (var element in Children)
{
element.Measure(availableSize);
}
return new Size();
}
protected override Size ArrangeOverride(Size finalSize)
{
if (ParentMap != null)
{
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());
}
}
}
}

View file

@ -0,0 +1,231 @@
// 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);
}
}
}
}

View file

@ -0,0 +1,214 @@
// 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();
}
}
}

View file

@ -0,0 +1,36 @@
// 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;
using Avalonia.Styling;
using System;
namespace MapControl
{
public partial class Tile
{
private void AnimateImageOpacity()
{
var animation = new Animation
{
Duration = MapBase.ImageFadeDuration,
Children =
{
new KeyFrame
{
KeyTime = TimeSpan.Zero,
Setters = { new Setter(Visual.OpacityProperty, 0d) }
},
new KeyFrame
{
KeyTime = MapBase.ImageFadeDuration,
Setters = { new Setter(Visual.OpacityProperty, 1d) }
}
}
};
_ = animation.RunAsync(Image);
}
}
}

View file

@ -0,0 +1,29 @@
// 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 Avalonia.Threading;
using System;
using System.IO;
using System.Threading.Tasks;
namespace MapControl
{
public partial class TileImageLoader
{
/// <summary>
/// Default folder where the Cache instance may save data, i.e. "C:\ProgramData\MapControl\TileCache".
/// </summary>
public static string DefaultCacheFolder =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl", "TileCache");
private static async Task LoadTileAsync(Tile tile, Func<Task<IImage>> loadImageFunc)
{
var image = await loadImageFunc().ConfigureAwait(false);
await Dispatcher.UIThread.InvokeAsync(() => tile.SetImageSource(image));
}
}
}

View file

@ -0,0 +1,35 @@
// 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();
}
}
}
}

View file

@ -0,0 +1,115 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// Copyright © 2024 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
namespace MapControl
{
/// <summary>
/// Defines the transformation between projected map coordinates in meters
/// and view coordinates in pixels.
/// </summary>
public class ViewTransform
{
public static double ZoomLevelToScale(double zoomLevel)
{
return 256d * Math.Pow(2d, zoomLevel) / (360d * MapProjection.Wgs84MeterPerDegree);
}
public static double ScaleToZoomLevel(double scale)
{
return Math.Log(scale * 360d * MapProjection.Wgs84MeterPerDegree / 256d, 2d);
}
/// <summary>
/// Gets the scaling factor from projected map coordinates to view coordinates,
/// as pixels per meter.
/// </summary>
public double Scale { get; private set; }
/// <summary>
/// Gets the rotation angle of the transform matrix.
/// </summary>
public double Rotation { get; private set; }
/// <summary>
/// Gets the transform matrix from projected map coordinates to view coordinates.
/// </summary>
public Matrix MapToViewMatrix { get; private set; }
/// <summary>
/// Gets the transform matrix from view coordinates to projected map coordinates.
/// </summary>
public Matrix ViewToMapMatrix { get; private set; }
/// <summary>
/// Transforms a Point from projected map coordinates to view coordinates.
/// </summary>
public Point MapToView(Point point)
{
return MapToViewMatrix.Transform(point);
}
/// <summary>
/// Transforms a Point from view coordinates to projected map coordinates.
/// </summary>
public Point ViewToMap(Point point)
{
return ViewToMapMatrix.Transform(point);
}
public void SetTransform(Point mapCenter, Point viewCenter, double scale, double rotation)
{
Scale = scale;
Rotation = ((rotation % 360d) + 360d) % 360d;
MapToViewMatrix = new Matrix(Scale, 0d, 0d, -Scale, -Scale * mapCenter.X, Scale * mapCenter.Y)
.Append(Matrix.CreateRotation(Rotation * Math.PI / 180d))
.Append(Matrix.CreateTranslation(viewCenter.X, viewCenter.Y));
ViewToMapMatrix = MapToViewMatrix.Invert();
}
public Matrix GetTileLayerTransform(double tileMatrixScale, Point tileMatrixTopLeft, Point tileMatrixOrigin)
{
// Tile matrix origin in map coordinates.
//
var mapOrigin = new Point(
tileMatrixTopLeft.X + tileMatrixOrigin.X / tileMatrixScale,
tileMatrixTopLeft.Y - tileMatrixOrigin.Y / tileMatrixScale);
// Tile matrix origin in view coordinates.
//
var viewOrigin = MapToView(mapOrigin);
var transformScale = Scale / tileMatrixScale;
return new Matrix(transformScale, 0d, 0d, transformScale, 0d, 0d)
.Append(Matrix.CreateRotation(Rotation * Math.PI / 180d))
.Append(Matrix.CreateTranslation(viewOrigin.X, viewOrigin.Y));
}
public Rect GetTileMatrixBounds(double tileMatrixScale, Point tileMatrixTopLeft, Size viewSize)
{
// View origin in map coordinates.
//
var origin = ViewToMap(new Point());
var transformScale = tileMatrixScale / Scale;
var transform = new Matrix(transformScale, 0d, 0d, transformScale, 0d, 0d)
.Append(Matrix.CreateRotation(-Rotation * Math.PI / 180d));
// Translate origin to tile matrix origin in pixels.
//
transform = transform.Append(Matrix.CreateTranslation(
tileMatrixScale * (origin.X - tileMatrixTopLeft.X),
tileMatrixScale * (tileMatrixTopLeft.Y - origin.Y)));
// Transform view bounds to tile pixel bounds.
//
return new Rect(0d, 0d, viewSize.Width, viewSize.Height).TransformToAABB(transform);
}
}
}