Attached property MapPanel.MapRect

This commit is contained in:
ClemensFischer 2026-01-28 15:55:30 +01:00
parent 881b39c4b4
commit 69b105c11f
12 changed files with 150 additions and 96 deletions

View file

@ -13,9 +13,12 @@ namespace MapControl
public static readonly AttachedProperty<BoundingBox> BoundingBoxProperty =
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel));
public static readonly AttachedProperty<MapRect> MapRectProperty =
DependencyPropertyHelper.RegisterAttached<MapRect>("MapRect", typeof(MapPanel));
static MapPanel()
{
AffectsParentArrange<MapPanel>(LocationProperty, BoundingBoxProperty);
AffectsParentArrange<MapPanel>(LocationProperty, BoundingBoxProperty, MapRectProperty);
}
public static MapBase GetParentMap(FrameworkElement element)

View file

@ -11,18 +11,13 @@ namespace MapControl
#else
[System.ComponentModel.TypeConverter(typeof(BoundingBoxConverter))]
#endif
public class BoundingBox(double latitude1, double longitude1, double latitude2, double longitude2, bool projectAxisAligned = false)
public class BoundingBox(double latitude1, double longitude1, double latitude2, double longitude2)
{
public double South { get; } = Math.Min(Math.Max(Math.Min(latitude1, latitude2), -90d), 90d);
public double North { get; } = Math.Min(Math.Max(Math.Max(latitude1, latitude2), -90d), 90d);
public double West { get; } = Math.Min(longitude1, longitude2);
public double East { get; } = Math.Max(longitude1, longitude2);
/// <summary>
/// Indicates whether a MapProjection projects the BoundingBox to an axis-aligned or skewed rectangle.
/// </summary>
public bool ProjectAxisAligned { get; } = projectAxisAligned;
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3}", South, West, North, East);

View file

@ -230,6 +230,14 @@ namespace MapControl
return MapProjection.MapToLocation(ViewTransform.ViewToMap(point));
}
/// <summary>
/// Gets a MapRect in projected map coordinates that covers a rectangle in view coordinates.
/// </summary>
public MapRect ViewToMapRect(Rect rect)
{
return new MapRect(ViewTransform.ViewToMapBounds(rect), MapProjection.Center);
}
/// <summary>
/// Gets a BoundingBox in geographic coordinates that covers a rectangle in view coordinates.
/// </summary>

View file

@ -180,7 +180,7 @@ namespace MapControl
updateTimer.Stop();
ImageSource image = null;
BoundingBox boundingBox = null;
MapRect mapRect = null;
if (ParentMap != null &&
(SupportedCrsIds == null || SupportedCrsIds.Contains(ParentMap.MapProjection.CrsId)))
@ -192,17 +192,13 @@ namespace MapControl
{
var x = (ParentMap.ActualWidth - width) / 2d;
var y = (ParentMap.ActualHeight - height) / 2d;
var mapRect = ParentMap.ViewTransform.ViewToMapBounds(new Rect(x, y, width, height));
boundingBox = ParentMap.MapProjection.MapToBoundingBox(mapRect, true);
if (boundingBox != null)
{
image = await GetImageAsync(mapRect, loadingProgress);
}
mapRect = ParentMap.ViewToMapRect(new Rect(x, y, width, height));
image = await GetImageAsync(mapRect.Rect, loadingProgress);
}
}
SwapImages(image, boundingBox);
SwapImages(image, mapRect);
updateInProgress = false;
}
else // update on next timer tick
@ -215,12 +211,12 @@ namespace MapControl
{
foreach (var image in Children.OfType<Image>())
{
image.ClearValue(BoundingBoxProperty);
image.ClearValue(MapRectProperty);
image.ClearValue(Image.SourceProperty);
}
}
private void SwapImages(ImageSource image, BoundingBox boundingBox)
private void SwapImages(ImageSource image, MapRect mapRect)
{
if (Children.Count >= 2)
{
@ -230,7 +226,7 @@ namespace MapControl
Children.Insert(1, topImage);
topImage.Source = image;
SetBoundingBox(topImage, boundingBox);
SetMapRect(topImage, mapRect);
if (MapBase.ImageFadeDuration > TimeSpan.Zero)
{

View file

@ -128,6 +128,22 @@ namespace MapControl
element.SetValue(BoundingBoxProperty, value);
}
/// <summary>
/// Gets the MapRect of an element.
/// </summary>
public static MapRect GetMapRect(FrameworkElement element)
{
return (MapRect)element.GetValue(MapRectProperty);
}
/// <summary>
/// Sets the MapRect of an element.
/// </summary>
public static void SetMapRect(FrameworkElement element, MapRect value)
{
element.SetValue(MapRectProperty, value);
}
/// <summary>
/// Gets the view position of an element with Location.
/// </summary>
@ -271,58 +287,66 @@ namespace MapControl
{
element.ClearValue(ViewPositionProperty);
var boundingBox = GetBoundingBox(element);
var mapRect = GetMapRect(element);
if (boundingBox != null)
if (mapRect != null)
{
ArrangeElement(element, boundingBox);
mapRect.Update(parentMap.MapProjection);
ArrangeElement(element, mapRect.Rect, null);
}
else
{
ArrangeElement(element, panelSize);
var boundingBox = GetBoundingBox(element);
if (boundingBox != null)
{
(var rect, var transform) = parentMap.MapProjection.BoundingBoxToMap(boundingBox);
if (rect.HasValue)
{
ArrangeElement(element, rect.Value, transform);
}
}
else
{
ArrangeElement(element, panelSize);
}
}
}
}
private void ArrangeElement(FrameworkElement element, BoundingBox boundingBox)
private void ArrangeElement(FrameworkElement element, Rect mapRect, Matrix? transform)
{
(var mapRect, var transform) = parentMap.MapProjection.BoundingBoxToMap(boundingBox);
var viewRect = GetViewRect(mapRect);
if (mapRect.HasValue)
element.Width = viewRect.Width;
element.Height = viewRect.Height;
element.Arrange(viewRect);
if (parentMap.ViewTransform.Rotation != 0d)
{
var viewRect = GetViewRect(mapRect.Value);
var t = transform ?? new Matrix(1d, 0d, 0d, 1d, 0d, 0d);
t.Rotate(parentMap.ViewTransform.Rotation);
transform = t;
}
element.Width = viewRect.Width;
element.Height = viewRect.Height;
element.Arrange(viewRect);
if (parentMap.ViewTransform.Rotation != 0d)
if (element.RenderTransform is MatrixTransform matrixTransform &&
!matrixTransform.Matrix.IsIdentity) // not default RenderTransform in WPF/UWP/WinUI
{
if (transform.HasValue)
{
if (!transform.HasValue)
{
transform = new Matrix(1d, 0d, 0d, 1d, 0d, 0d);
}
transform.Value.Rotate(parentMap.ViewTransform.Rotation);
matrixTransform.Matrix = transform.Value;
}
if (element.RenderTransform is MatrixTransform matrixTransform &&
!matrixTransform.Matrix.IsIdentity) // not default RenderTransform in WPF/UWP/WinUI
else
{
if (transform.HasValue)
{
matrixTransform.Matrix = transform.Value;
}
else
{
element.ClearValue(RenderTransformProperty);
}
}
else if (transform.HasValue)
{
element.SetRenderTransform(new MatrixTransform { Matrix = transform.Value }, true);
element.ClearValue(RenderTransformProperty);
}
}
else if (transform.HasValue)
{
element.SetRenderTransform(new MatrixTransform { Matrix = transform.Value }, true);
}
}
private static void ArrangeElement(FrameworkElement element, Point position)

View file

@ -64,7 +64,7 @@ namespace MapControl
/// </summary>
public Location Center
{
get => center ??= new Location();
get => center;
set
{
updateCenter = true;
@ -83,6 +83,7 @@ namespace MapControl
protected void EnableCenterUpdates()
{
updateCenter = true;
SetCenter(new Location());
}
/// <summary>
@ -138,51 +139,39 @@ namespace MapControl
public Location MapToLocation(Point point) => MapToLocation(point.X, point.Y);
/// <summary>
/// Transforms a BoundingBox in geographic coordinates to a Rect in projected map coordinates
/// with an optional transform Matrix when the BoundingBox is not projected axis-aligned.
/// Returns (null, null) when the BoundingBox can not be transformed.
/// Transforms a BoundingBox in geographic coordinates to a Rect in projected map coordinates with
/// an optional transform Matrix. Returns (null, null) when the BoundingBox can not be transformed.
/// </summary>
public (Rect?, Matrix?) BoundingBoxToMap(BoundingBox boundingBox)
{
Rect? rect = null;
Matrix? transform = null;
var sw = LocationToMap(boundingBox.South, boundingBox.West);
var se = LocationToMap(boundingBox.South, boundingBox.East);
var nw = LocationToMap(boundingBox.North, boundingBox.West);
var ne = LocationToMap(boundingBox.North, boundingBox.East);
if (sw.HasValue && ne.HasValue)
if (sw.HasValue && se.HasValue && nw.HasValue && ne.HasValue)
{
if (boundingBox.ProjectAxisAligned)
var south = new Point((sw.Value.X + se.Value.X) / 2d, (sw.Value.Y + se.Value.Y) / 2d); // south midpoint
var north = new Point((nw.Value.X + ne.Value.X) / 2d, (nw.Value.Y + ne.Value.Y) / 2d); // north midpoint
var west = new Point((nw.Value.X + sw.Value.X) / 2d, (nw.Value.Y + sw.Value.Y) / 2d); // west midpoint
var east = new Point((ne.Value.X + se.Value.X) / 2d, (ne.Value.Y + se.Value.Y) / 2d); // east midpoint
var center = new Point((west.X + east.X) / 2d, (west.Y + east.Y) / 2d); // midpoint of segment west-east
var dx1 = east.X - west.X;
var dy1 = east.Y - west.Y;
var dx2 = north.X - south.X;
var dy2 = north.Y - south.Y;
var width = Math.Sqrt(dx1 * dx1 + dy1 * dy1); // distance west-east
var height = Math.Sqrt(dx2 * dx2 + dy2 * dy2); // distance south-north
rect = new Rect(center.X - width / 2d, center.Y - height / 2d, width, height);
if (dy1 != 0d || dx2 != 0d)
{
rect = new Rect(sw.Value, ne.Value);
}
else
{
var se = LocationToMap(boundingBox.South, boundingBox.East);
var nw = LocationToMap(boundingBox.North, boundingBox.West);
if (se.HasValue && nw.HasValue)
{
var south = new Point((sw.Value.X + se.Value.X) / 2d, (sw.Value.Y + se.Value.Y) / 2d); // south midpoint
var north = new Point((nw.Value.X + ne.Value.X) / 2d, (nw.Value.Y + ne.Value.Y) / 2d); // north midpoint
var west = new Point((nw.Value.X + sw.Value.X) / 2d, (nw.Value.Y + sw.Value.Y) / 2d); // west midpoint
var east = new Point((ne.Value.X + se.Value.X) / 2d, (ne.Value.Y + se.Value.Y) / 2d); // east midpoint
var center = new Point((west.X + east.X) / 2d, (west.Y + east.Y) / 2d); // midpoint of segment west-east
var dx1 = east.X - west.X;
var dy1 = east.Y - west.Y;
var dx2 = north.X - south.X;
var dy2 = north.Y - south.Y;
var width = Math.Sqrt(dx1 * dx1 + dy1 * dy1); // distance west-east
var height = Math.Sqrt(dx2 * dx2 + dy2 * dy2); // distance south-north
rect = new Rect(center.X - width / 2d, center.Y - height / 2d, width, height);
if (dy1 != 0d || dx2 != 0d)
{
// Skew matrix with skewX = Atan(-dx2 / dy2) and skewY = Atan(-dy1 / dx1).
//
transform = new Matrix(1d, -dy1 / dx1, -dx2 / dy2, 1d, 0d, 0d);
}
}
// Skew matrix with skewX = Atan(-dx2 / dy2) and skewY = Atan(-dy1 / dx1).
//
transform = new Matrix(1d, -dy1 / dx1, -dx2 / dy2, 1d, 0d, 0d);
}
}
@ -193,14 +182,12 @@ namespace MapControl
/// Transforms a Rect in projected map coordinates to a BoundingBox in geographic coordinates.
/// Returns null when the Rect can not be transformed.
/// </summary>
public BoundingBox MapToBoundingBox(Rect rect, bool axisAligned = false)
public BoundingBox MapToBoundingBox(Rect rect)
{
var sw = MapToLocation(rect.X, rect.Y);
var ne = MapToLocation(rect.X + rect.Width, rect.Y + rect.Height);
return sw != null && ne != null
? new BoundingBox(sw.Latitude, sw.Longitude, ne.Latitude, ne.Longitude, axisAligned)
: null;
return sw != null && ne != null ? new BoundingBox(sw.Latitude, sw.Longitude, ne.Latitude, ne.Longitude) : null;
}
public override string ToString()

View file

@ -0,0 +1,27 @@
#if WPF
using System.Windows;
#elif AVALONIA
using Avalonia;
#endif
namespace MapControl
{
public class MapRect(Rect rect, Location origin)
{
public Rect Rect { get; private set; } = rect;
public Location Origin { get; private set; } = origin;
public void Update(MapProjection projection)
{
Point? origin;
if (Origin != null && projection.Center != null &&
!Origin.Equals(projection.Center) &&
(origin = projection.LocationToMap(Origin)).HasValue)
{
Rect = new Rect(Rect.X + origin.Value.X, Rect.Y + origin.Value.Y, Rect.Width, Rect.Height);
Origin = projection.Center;
}
}
}
}

View file

@ -329,8 +329,16 @@ namespace MapControl
if (crs.StartsWith("AUTO2:") || crs.StartsWith("AUTO:"))
{
crs = string.Format(CultureInfo.InvariantCulture, "{0},1,{1:0.########},{2:0.########}",
crs, projection.Center.Longitude, projection.Center.Latitude);
var lon = 0d;
var lat = 0d; ;
if (projection.Center != null)
{
lon = projection.Center.Longitude;
lat = projection.Center.Latitude;
}
crs = string.Format(CultureInfo.InvariantCulture, "{0},1,{1:0.########},{2:0.########}", crs, lon, lat);
}
return crs;

View file

@ -15,6 +15,10 @@ namespace MapControl
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel), null,
FrameworkPropertyMetadataOptions.AffectsParentArrange);
public static readonly DependencyProperty MapRectProperty =
DependencyPropertyHelper.RegisterAttached<MapRect>("MapRect", typeof(MapPanel), null,
FrameworkPropertyMetadataOptions.AffectsParentArrange);
public static MapBase GetParentMap(FrameworkElement element)
{
return (MapBase)element.GetValue(ParentMapProperty);

View file

@ -21,6 +21,10 @@ namespace MapControl
DependencyPropertyHelper.RegisterAttached<BoundingBox>("BoundingBox", typeof(MapPanel), null,
(element, oldValue, newValue) => (element.Parent as MapPanel)?.InvalidateArrange());
public static readonly DependencyProperty MapRectProperty =
DependencyPropertyHelper.RegisterAttached<MapRect>("MapRect", typeof(MapPanel), null,
(element, oldValue, newValue) => (element.Parent as MapPanel)?.InvalidateArrange());
public static void InitMapElement(FrameworkElement element)
{
// Workaround for missing property value inheritance.

View file

@ -14,7 +14,6 @@ namespace MapControl.Projections
public Wgs84OrthographicProjection()
{
EnableCenterUpdates();
CenterChanged();
}
protected override void CenterChanged()

View file

@ -14,7 +14,6 @@ namespace MapControl.Projections
public Wgs84StereographicProjection()
{
EnableCenterUpdates();
CenterChanged();
}
protected override void CenterChanged()