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

589 lines
21 KiB
C#
Raw Normal View History

2025-02-27 18:46:32 +01:00
using System;
2024-05-22 11:25:32 +02:00
#if WPF
using System.Windows;
using System.Windows.Media;
2021-11-17 23:17:11 +01:00
#elif UWP
using Windows.UI.Xaml;
2024-05-21 13:51:10 +02:00
using Windows.UI.Xaml.Media;
2024-05-22 11:25:32 +02:00
#elif WINUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
#endif
namespace MapControl
{
2024-05-21 13:51:10 +02:00
public interface IMapLayer : IMapElement
{
2024-05-21 13:51:10 +02:00
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
{
2024-08-30 10:27:59 +02:00
public static double ZoomLevelToScale(double zoomLevel)
2024-09-02 15:49:53 +02:00
{
return 256d * Math.Pow(2d, zoomLevel) / (360d * MapProjection.Wgs84MeterPerDegree);
}
2024-08-29 21:35:58 +02:00
2024-08-30 10:27:59 +02:00
public static double ScaleToZoomLevel(double scale)
2024-09-02 15:49:53 +02:00
{
return Math.Log(scale * 360d * MapProjection.Wgs84MeterPerDegree / 256d, 2d);
}
2024-08-29 21:35:58 +02:00
2024-05-21 13:51:10 +02:00
public static TimeSpan ImageFadeDuration { get; set; } = TimeSpan.FromSeconds(0.1);
public static readonly DependencyProperty AnimationDurationProperty =
DependencyPropertyHelper.Register<MapBase, TimeSpan>(nameof(AnimationDuration), TimeSpan.FromSeconds(0.3));
2024-05-21 13:51:10 +02:00
public static readonly DependencyProperty MapLayerProperty =
2024-05-27 11:18:14 +02:00
DependencyPropertyHelper.Register<MapBase, FrameworkElement>(nameof(MapLayer), null,
2024-05-21 13:51:10 +02:00
(map, oldValue, newValue) => map.MapLayerPropertyChanged(oldValue, newValue));
2024-05-21 13:51:10 +02:00
public static readonly DependencyProperty MapProjectionProperty =
2024-05-23 18:22:52 +02:00
DependencyPropertyHelper.Register<MapBase, MapProjection>(nameof(MapProjection), new WebMercatorProjection(),
2024-05-21 13:51:10 +02:00
(map, oldValue, newValue) => map.MapProjectionPropertyChanged(newValue));
public static readonly DependencyProperty ProjectionCenterProperty =
2024-05-23 18:22:52 +02:00
DependencyPropertyHelper.Register<MapBase, Location>(nameof(ProjectionCenter), null,
2024-05-21 13:51:10 +02:00
(map, oldValue, newValue) => map.ProjectionCenterPropertyChanged());
private Location transformCenter;
private Point viewCenter;
private double centerLongitude;
2024-05-24 09:13:41 +02:00
private double maxLatitude = 85.05112878; // default WebMercatorProjection
2024-05-21 13:51:10 +02:00
private bool internalPropertyChange;
/// <summary>
2024-05-21 13:51:10 +02:00
/// Raised when the current map viewport has changed.
/// </summary>
2024-05-21 13:51:10 +02:00
public event EventHandler<ViewportChangedEventArgs> ViewportChanged;
/// <summary>
/// Gets or sets the map foreground Brush.
/// </summary>
public Brush Foreground
{
2024-05-21 13:51:10 +02:00
get => (Brush)GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
2022-11-02 19:49:18 +01:00
/// <summary>
2025-06-12 00:31:01 +02:00
/// Gets or sets the duration of the Center, ZoomLevel and Heading animations.
2024-05-21 13:51:10 +02:00
/// The default value is 0.3 seconds.
2022-11-02 19:49:18 +01:00
/// </summary>
2024-05-21 13:51:10 +02:00
public TimeSpan AnimationDuration
{
get => (TimeSpan)GetValue(AnimationDurationProperty);
set => SetValue(AnimationDurationProperty, value);
}
2022-11-02 19:49:18 +01:00
/// <summary>
2024-05-21 13:51:10 +02:00
/// 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>
2024-05-27 11:18:14 +02:00
public FrameworkElement MapLayer
{
2024-05-27 11:18:14 +02:00
get => (FrameworkElement)GetValue(MapLayerProperty);
2024-05-21 13:51:10 +02:00
set => SetValue(MapLayerProperty, value);
}
2024-05-19 23:22:54 +02:00
2024-05-21 13:51:10 +02:00
/// <summary>
/// Gets or sets the MapProjection used by the map control.
/// </summary>
public MapProjection MapProjection
{
get => (MapProjection)GetValue(MapProjectionProperty);
set => SetValue(MapProjectionProperty, value);
}
2024-05-19 23:22:54 +02:00
2024-05-21 13:51:10 +02:00
/// <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);
}
2024-05-21 13:51:10 +02:00
/// <summary>
/// Gets or sets the location of the center point of the map.
/// </summary>
public Location Center
{
2024-05-21 13:51:10 +02:00
get => (Location)GetValue(CenterProperty);
set => SetValue(CenterProperty, value);
}
2022-03-05 18:40:57 +01:00
2024-05-21 13:51:10 +02:00
/// <summary>
/// Gets or sets the target value of a Center animation.
/// </summary>
public Location TargetCenter
{
get => (Location)GetValue(TargetCenterProperty);
set => SetValue(TargetCenterProperty, value);
}
2022-03-05 18:40:57 +01:00
2024-05-21 13:51:10 +02:00
/// <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);
}
2022-03-05 18:40:57 +01:00
2024-05-21 13:51:10 +02:00
/// <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);
}
2022-03-05 18:40:57 +01:00
2024-05-21 13:51:10 +02:00
/// <summary>
/// Gets or sets the map zoom level.
/// </summary>
public double ZoomLevel
{
get => (double)GetValue(ZoomLevelProperty);
set => SetValue(ZoomLevelProperty, value);
}
2024-05-21 13:51:10 +02:00
/// <summary>
/// Gets or sets the target value of a ZoomLevel animation.
/// </summary>
public double TargetZoomLevel
2024-05-20 08:33:30 +02:00
{
2024-05-21 13:51:10 +02:00
get => (double)GetValue(TargetZoomLevelProperty);
set => SetValue(TargetZoomLevelProperty, value);
}
2024-05-20 08:33:30 +02:00
2024-05-21 13:51:10 +02:00
/// <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>
2024-05-22 09:42:21 +02:00
/// Gets the map scale as horizontal and vertical scaling factors from meters to
/// view coordinates at the specified location.
2024-05-21 13:51:10 +02:00
/// </summary>
public Point GetScale(Location location)
{
2024-05-22 09:42:21 +02:00
return ViewTransform.GetMapScale(MapProjection.GetRelativeScale(location));
}
2024-05-21 13:51:10 +02:00
2024-05-22 09:42:21 +02:00
/// <summary>
/// Gets a transform Matrix from meters to view coordinates for scaling and rotating
/// objects that are anchored at a Location.
/// </summary>
public Matrix GetMapTransform(Location location)
{
return ViewTransform.GetMapTransform(MapProjection.GetRelativeScale(location));
2024-05-21 13:51:10 +02:00
}
/// <summary>
/// Transforms a Location in geographic coordinates to a Point in view coordinates.
/// </summary>
public Point? LocationToView(Location location)
{
var point = MapProjection.LocationToMap(location);
2024-05-20 08:33:30 +02:00
2024-08-28 20:25:36 +02:00
if (point.HasValue)
2024-05-20 08:33:30 +02:00
{
2024-08-28 20:25:36 +02:00
point = ViewTransform.MapToView(point.Value);
2024-05-20 08:33:30 +02:00
}
2024-08-28 20:25:36 +02:00
return point;
2024-05-20 08:33:30 +02:00
}
2024-05-21 13:51:10 +02:00
/// <summary>
/// Transforms a Point in view coordinates to a Location in geographic coordinates.
/// </summary>
public Location ViewToLocation(Point point)
{
2024-05-21 13:51:10 +02:00
return MapProjection.MapToLocation(ViewTransform.ViewToMap(point));
}
/// <summary>
2024-08-29 10:05:14 +02:00
/// Gets a BoundingBox in geographic coordinates that covers a Rect in view coordinates.
2024-05-21 13:51:10 +02:00
/// </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(ActualWidth / 2d, ActualHeight / 2d);
2024-05-21 13:51:10 +02:00
}
/// <summary>
/// Resets the temporary transform center point set by SetTransformCenter.
/// </summary>
public void ResetTransformCenter()
{
transformCenter = null;
viewCenter = new Point(ActualWidth / 2d, ActualHeight / 2d);
2024-05-21 13:51:10 +02:00
}
2024-05-20 08:33:30 +02:00
2024-09-09 21:50:29 +02:00
/// <summary>
/// Changes the Center property according to the specified translation in view coordinates.
/// </summary>
public void TranslateMap(Point translation)
{
2024-05-21 13:51:10 +02:00
if (translation.X != 0d || translation.Y != 0d)
{
var center = ViewToLocation(new Point(viewCenter.X - translation.X, viewCenter.Y - translation.Y));
if (center != null)
{
2024-05-21 13:51:10 +02:00
Center = center;
}
}
}
2024-05-21 13:51:10 +02:00
/// <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)
{
2025-01-06 15:33:12 +01:00
if (rotation == 0d && scale == 1d)
{
TranslateMap(translation);
}
else
{
2024-05-21 13:51:10 +02:00
SetTransformCenter(center);
viewCenter = new Point(viewCenter.X + translation.X, viewCenter.Y + translation.Y);
if (rotation != 0d)
{
2025-06-12 00:31:01 +02:00
var heading = CoerceHeadingProperty(Heading - rotation);
2024-05-21 13:51:10 +02:00
SetValueInternal(HeadingProperty, heading);
SetValueInternal(TargetHeadingProperty, heading);
}
2024-05-21 13:51:10 +02:00
if (scale != 1d)
{
2025-06-12 00:31:01 +02:00
var zoomLevel = CoerceZoomLevelProperty(ZoomLevel + Math.Log(scale, 2d));
2024-05-21 13:51:10 +02:00
SetValueInternal(ZoomLevelProperty, zoomLevel);
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
}
2024-05-21 13:51:10 +02:00
UpdateTransform(true);
}
}
2024-05-21 13:51:10 +02:00
/// <summary>
2025-06-12 00:31:01 +02:00
/// Sets the ZoomLevel or TargetZoomLevel property while retaining
/// the specified center point in view coordinates.
2024-05-21 13:51:10 +02:00
/// </summary>
2025-06-12 00:31:01 +02:00
public void ZoomMap(Point center, double zoomLevel, bool animated = true)
{
2024-05-21 13:51:10 +02:00
zoomLevel = CoerceZoomLevelProperty(zoomLevel);
2025-07-16 10:25:55 +02:00
if (animated || zoomLevelAnimation != null)
{
2025-06-12 00:31:01 +02:00
if (TargetZoomLevel != zoomLevel)
{
SetTransformCenter(center);
TargetZoomLevel = zoomLevel;
}
}
else
{
if (ZoomLevel != zoomLevel)
{
SetTransformCenter(center);
SetValueInternal(ZoomLevelProperty, zoomLevel);
SetValueInternal(TargetZoomLevelProperty, zoomLevel);
UpdateTransform(true);
}
}
}
2024-05-21 13:51:10 +02:00
/// <summary>
2025-08-02 18:18:24 +02:00
/// Sets the TargetZoomLevel and TargetCenter properties so that the specified BoundingBox
2024-05-21 13:51:10 +02:00
/// fits into the current view. The TargetHeading property is set to zero.
/// </summary>
public void ZoomToBounds(BoundingBox boundingBox)
{
2024-09-08 14:03:55 +02:00
var mapRect = MapProjection.BoundingBoxToMap(boundingBox);
2024-05-21 13:51:10 +02:00
2024-09-08 14:03:55 +02:00
if (mapRect.HasValue)
{
2024-09-08 14:03:55 +02:00
var rectCenter = new Point(mapRect.Value.X + mapRect.Value.Width / 2d, mapRect.Value.Y + mapRect.Value.Height / 2d);
2024-05-21 13:51:10 +02:00
var targetCenter = MapProjection.MapToLocation(rectCenter);
if (targetCenter != null)
{
2024-09-08 14:03:55 +02:00
var scale = Math.Min(ActualWidth / mapRect.Value.Width, ActualHeight / mapRect.Value.Height);
2024-08-29 21:35:58 +02:00
TargetZoomLevel = ScaleToZoomLevel(scale);
2024-05-21 13:51:10 +02:00
TargetCenter = targetCenter;
TargetHeading = 0d;
}
}
}
2024-08-30 17:35:30 +02:00
internal bool InsideViewBounds(Point point)
2024-09-02 15:49:53 +02:00
{
return point.X >= 0d && point.Y >= 0d && point.X <= ActualWidth && point.Y <= ActualHeight;
}
2024-08-29 23:56:29 +02:00
internal double CoerceLongitude(double longitude)
{
2024-05-21 13:51:10 +02:00
var offset = longitude - Center.Longitude;
2021-01-07 22:52:41 +01:00
2024-05-21 13:51:10 +02:00
if (offset > 180d)
{
longitude = Center.Longitude + (offset % 360d) - 360d;
}
2024-05-21 13:51:10 +02:00
else if (offset < -180d)
{
2024-05-21 13:51:10 +02:00
longitude = Center.Longitude + (offset % 360d) + 360d;
}
2024-05-21 13:51:10 +02:00
return longitude;
}
2024-05-21 13:51:10 +02:00
private Location CoerceCenterProperty(Location center)
{
2024-05-21 13:51:10 +02:00
if (center == null)
{
2024-05-21 13:51:10 +02:00
center = new Location();
}
2024-05-21 13:51:10 +02:00
else if (
center.Latitude < -maxLatitude || center.Latitude > maxLatitude ||
center.Longitude < -180d || center.Longitude > 180d)
{
2024-05-21 13:51:10 +02:00
center = new Location(
Math.Min(Math.Max(center.Latitude, -maxLatitude), maxLatitude),
Location.NormalizeLongitude(center.Longitude));
}
2024-05-21 13:51:10 +02:00
return center;
}
2024-05-21 13:51:10 +02:00
private double CoerceMinZoomLevelProperty(double minZoomLevel)
2024-05-20 08:33:30 +02:00
{
2024-05-21 13:51:10 +02:00
return Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel);
}
2024-05-20 08:33:30 +02:00
2024-05-21 13:51:10 +02:00
private double CoerceMaxZoomLevelProperty(double maxZoomLevel)
{
return Math.Max(maxZoomLevel, MinZoomLevel);
}
2024-05-20 08:33:30 +02:00
2024-05-21 13:51:10 +02:00
private double CoerceZoomLevelProperty(double zoomLevel)
{
return Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
2024-05-20 08:33:30 +02:00
}
2024-05-21 13:51:10 +02:00
private double CoerceHeadingProperty(double heading)
{
2024-05-21 13:51:10 +02:00
return ((heading % 360d) + 360d) % 360d;
}
2024-05-20 08:33:30 +02:00
2024-05-21 13:51:10 +02:00
private void SetValueInternal(DependencyProperty property, object value)
{
internalPropertyChange = true;
SetValue(property, value);
internalPropertyChange = false;
}
2024-05-27 11:18:14 +02:00
private void MapLayerPropertyChanged(FrameworkElement oldLayer, FrameworkElement newLayer)
{
2024-05-21 13:51:10 +02:00
if (oldLayer != null)
{
2025-08-08 20:31:12 +02:00
if (Children.Count > 0 && Children[0] == oldLayer)
{
Children.RemoveAt(0);
}
2024-05-21 13:51:10 +02:00
if (oldLayer is IMapLayer mapLayer)
{
2024-05-21 13:51:10 +02:00
if (mapLayer.MapBackground != null)
{
2024-05-21 13:51:10 +02:00
ClearValue(BackgroundProperty);
}
2024-05-21 13:51:10 +02:00
if (mapLayer.MapForeground != null)
{
2024-05-21 13:51:10 +02:00
ClearValue(ForegroundProperty);
}
}
}
2024-05-21 13:51:10 +02:00
if (newLayer != null)
{
2025-08-08 20:31:12 +02:00
if (Children.Count == 0 || Children[0] != newLayer)
{
Children.Insert(0, newLayer);
}
2017-11-10 17:26:15 +01:00
2024-05-21 13:51:10 +02:00
if (newLayer is IMapLayer mapLayer)
{
if (mapLayer.MapBackground != null)
{
Background = mapLayer.MapBackground;
}
if (mapLayer.MapForeground != null)
{
Foreground = mapLayer.MapForeground;
}
}
}
}
2024-05-21 13:51:10 +02:00
private void MapProjectionPropertyChanged(MapProjection projection)
{
2024-05-21 13:51:10 +02:00
maxLatitude = 90d;
if (projection.Type <= MapProjectionType.NormalCylindrical)
{
2024-05-21 13:51:10 +02:00
var maxLocation = projection.MapToLocation(new Point(0d, 180d * MapProjection.Wgs84MeterPerDegree));
2017-11-10 17:26:15 +01:00
2024-05-21 13:51:10 +02:00
if (maxLocation != null && maxLocation.Latitude < 90d)
{
maxLatitude = maxLocation.Latitude;
2024-05-21 13:51:10 +02:00
Center = CoerceCenterProperty(Center);
}
}
2024-05-21 13:51:10 +02:00
ResetTransformCenter();
UpdateTransform(false, true);
}
2024-05-21 13:51:10 +02:00
private void ProjectionCenterPropertyChanged()
2024-05-20 08:33:30 +02:00
{
2024-05-21 13:51:10 +02:00
ResetTransformCenter();
UpdateTransform();
2024-05-20 08:33:30 +02:00
}
2024-05-21 13:51:10 +02:00
private void UpdateTransform(bool resetTransformCenter = false, bool projectionChanged = false)
{
2024-05-21 13:51:10 +02:00
var transformCenterChanged = false;
2024-08-29 21:35:58 +02:00
var viewScale = ZoomLevelToScale(ZoomLevel);
2024-05-21 13:51:10 +02:00
var projection = MapProjection;
2024-05-20 08:33:30 +02:00
2024-05-21 13:51:10 +02:00
projection.Center = ProjectionCenter ?? Center;
2024-05-21 13:51:10 +02:00
var mapCenter = projection.LocationToMap(transformCenter ?? Center);
2024-05-21 13:51:10 +02:00
if (mapCenter.HasValue)
{
2024-05-21 13:51:10 +02:00
ViewTransform.SetTransform(mapCenter.Value, viewCenter, viewScale, -Heading);
2024-05-21 13:51:10 +02:00
if (transformCenter != null)
{
var center = ViewToLocation(new Point(ActualWidth / 2d, ActualHeight / 2d));
2024-05-21 13:51:10 +02:00
if (center != null)
{
2024-05-21 13:51:10 +02:00
var centerLatitude = center.Latitude;
var centerLongitude = Location.NormalizeLongitude(center.Longitude);
2024-05-21 13:51:10 +02:00
if (centerLatitude < -maxLatitude || centerLatitude > maxLatitude)
{
centerLatitude = Math.Min(Math.Max(centerLatitude, -maxLatitude), maxLatitude);
resetTransformCenter = true;
}
2024-05-21 13:51:10 +02:00
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;
2024-05-21 13:51:10 +02:00
ResetTransformCenter();
2017-11-10 17:26:15 +01:00
2024-05-21 13:51:10 +02:00
projection.Center = ProjectionCenter ?? Center;
mapCenter = projection.LocationToMap(center);
if (mapCenter.HasValue)
{
ViewTransform.SetTransform(mapCenter.Value, viewCenter, viewScale, -Heading);
}
}
}
}
2024-05-21 13:51:10 +02:00
2024-05-22 09:42:21 +02:00
ViewScale = ViewTransform.Scale;
2024-05-21 13:51:10 +02:00
// 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));
}
}
2024-05-21 13:51:10 +02:00
protected override void OnViewportChanged(ViewportChangedEventArgs e)
{
2024-05-21 13:51:10 +02:00
base.OnViewportChanged(e);
2024-05-21 13:51:10 +02:00
ViewportChanged?.Invoke(this, e);
}
}
}