diff --git a/MapControl/Avalonia/MapPanel.Avalonia.cs b/MapControl/Avalonia/MapPanel.Avalonia.cs index a2f10119..7686c819 100644 --- a/MapControl/Avalonia/MapPanel.Avalonia.cs +++ b/MapControl/Avalonia/MapPanel.Avalonia.cs @@ -13,9 +13,12 @@ namespace MapControl public static readonly AttachedProperty BoundingBoxProperty = DependencyPropertyHelper.RegisterAttached("BoundingBox", typeof(MapPanel)); + public static readonly AttachedProperty MapRectProperty = + DependencyPropertyHelper.RegisterAttached("MapRect", typeof(MapPanel)); + static MapPanel() { - AffectsParentArrange(LocationProperty, BoundingBoxProperty); + AffectsParentArrange(LocationProperty, BoundingBoxProperty, MapRectProperty); } public static MapBase GetParentMap(FrameworkElement element) diff --git a/MapControl/Shared/BoundingBox.cs b/MapControl/Shared/BoundingBox.cs index 42f1e66c..7295e820 100644 --- a/MapControl/Shared/BoundingBox.cs +++ b/MapControl/Shared/BoundingBox.cs @@ -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); - /// - /// Indicates whether a MapProjection projects the BoundingBox to an axis-aligned or skewed rectangle. - /// - public bool ProjectAxisAligned { get; } = projectAxisAligned; - public override string ToString() { return string.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3}", South, West, North, East); diff --git a/MapControl/Shared/MapBase.cs b/MapControl/Shared/MapBase.cs index 321936d9..e83d4c3e 100644 --- a/MapControl/Shared/MapBase.cs +++ b/MapControl/Shared/MapBase.cs @@ -230,6 +230,14 @@ namespace MapControl return MapProjection.MapToLocation(ViewTransform.ViewToMap(point)); } + /// + /// Gets a MapRect in projected map coordinates that covers a rectangle in view coordinates. + /// + public MapRect ViewToMapRect(Rect rect) + { + return new MapRect(ViewTransform.ViewToMapBounds(rect), MapProjection.Center); + } + /// /// Gets a BoundingBox in geographic coordinates that covers a rectangle in view coordinates. /// diff --git a/MapControl/Shared/MapImageLayer.cs b/MapControl/Shared/MapImageLayer.cs index a1c37032..6e3117ad 100644 --- a/MapControl/Shared/MapImageLayer.cs +++ b/MapControl/Shared/MapImageLayer.cs @@ -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.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) { diff --git a/MapControl/Shared/MapPanel.cs b/MapControl/Shared/MapPanel.cs index e1d7e9ef..01aad839 100644 --- a/MapControl/Shared/MapPanel.cs +++ b/MapControl/Shared/MapPanel.cs @@ -128,6 +128,22 @@ namespace MapControl element.SetValue(BoundingBoxProperty, value); } + /// + /// Gets the MapRect of an element. + /// + public static MapRect GetMapRect(FrameworkElement element) + { + return (MapRect)element.GetValue(MapRectProperty); + } + + /// + /// Sets the MapRect of an element. + /// + public static void SetMapRect(FrameworkElement element, MapRect value) + { + element.SetValue(MapRectProperty, value); + } + /// /// Gets the view position of an element with Location. /// @@ -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) diff --git a/MapControl/Shared/MapProjection.cs b/MapControl/Shared/MapProjection.cs index 57916e9c..9b6bf52e 100644 --- a/MapControl/Shared/MapProjection.cs +++ b/MapControl/Shared/MapProjection.cs @@ -64,7 +64,7 @@ namespace MapControl /// 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()); } /// @@ -138,51 +139,39 @@ namespace MapControl public Location MapToLocation(Point point) => MapToLocation(point.X, point.Y); /// - /// 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. /// 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. /// - 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() diff --git a/MapControl/Shared/MapRect.cs b/MapControl/Shared/MapRect.cs new file mode 100644 index 00000000..099d82e0 --- /dev/null +++ b/MapControl/Shared/MapRect.cs @@ -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; + } + } + } +} diff --git a/MapControl/Shared/WmsImageLayer.cs b/MapControl/Shared/WmsImageLayer.cs index ca830d59..ef5aed00 100644 --- a/MapControl/Shared/WmsImageLayer.cs +++ b/MapControl/Shared/WmsImageLayer.cs @@ -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; diff --git a/MapControl/WPF/MapPanel.WPF.cs b/MapControl/WPF/MapPanel.WPF.cs index 4e0e923e..48a9cded 100644 --- a/MapControl/WPF/MapPanel.WPF.cs +++ b/MapControl/WPF/MapPanel.WPF.cs @@ -15,6 +15,10 @@ namespace MapControl DependencyPropertyHelper.RegisterAttached("BoundingBox", typeof(MapPanel), null, FrameworkPropertyMetadataOptions.AffectsParentArrange); + public static readonly DependencyProperty MapRectProperty = + DependencyPropertyHelper.RegisterAttached("MapRect", typeof(MapPanel), null, + FrameworkPropertyMetadataOptions.AffectsParentArrange); + public static MapBase GetParentMap(FrameworkElement element) { return (MapBase)element.GetValue(ParentMapProperty); diff --git a/MapControl/WinUI/MapPanel.WinUI.cs b/MapControl/WinUI/MapPanel.WinUI.cs index 0f78bb05..b6d8ecc3 100644 --- a/MapControl/WinUI/MapPanel.WinUI.cs +++ b/MapControl/WinUI/MapPanel.WinUI.cs @@ -21,6 +21,10 @@ namespace MapControl DependencyPropertyHelper.RegisterAttached("BoundingBox", typeof(MapPanel), null, (element, oldValue, newValue) => (element.Parent as MapPanel)?.InvalidateArrange()); + public static readonly DependencyProperty MapRectProperty = + DependencyPropertyHelper.RegisterAttached("MapRect", typeof(MapPanel), null, + (element, oldValue, newValue) => (element.Parent as MapPanel)?.InvalidateArrange()); + public static void InitMapElement(FrameworkElement element) { // Workaround for missing property value inheritance. diff --git a/MapProjections/Shared/Wgs84OrthographicProjection.cs b/MapProjections/Shared/Wgs84OrthographicProjection.cs index f4a29487..c90627e9 100644 --- a/MapProjections/Shared/Wgs84OrthographicProjection.cs +++ b/MapProjections/Shared/Wgs84OrthographicProjection.cs @@ -14,7 +14,6 @@ namespace MapControl.Projections public Wgs84OrthographicProjection() { EnableCenterUpdates(); - CenterChanged(); } protected override void CenterChanged() diff --git a/MapProjections/Shared/Wgs84StereographicProjection.cs b/MapProjections/Shared/Wgs84StereographicProjection.cs index 55cc7db9..8f93b01f 100644 --- a/MapProjections/Shared/Wgs84StereographicProjection.cs +++ b/MapProjections/Shared/Wgs84StereographicProjection.cs @@ -14,7 +14,6 @@ namespace MapControl.Projections public Wgs84StereographicProjection() { EnableCenterUpdates(); - CenterChanged(); } protected override void CenterChanged()