From 8efcee2585f5a567fb963a2fcf72e315a9aa1724 Mon Sep 17 00:00:00 2001 From: ClemensFischer Date: Mon, 9 Sep 2024 16:44:45 +0200 Subject: [PATCH] LatLonBox transformation --- MapControl/Shared/AzimuthalProjection.cs | 12 +-- MapControl/Shared/BoundingBox.cs | 10 +-- MapControl/Shared/GeoImage.cs | 14 +++- MapControl/Shared/GroundOverlay.cs | 12 +-- MapControl/Shared/LatLonBox.cs | 29 ++++++++ MapControl/Shared/MapPanel.cs | 36 ++++++--- MapControl/Shared/MapProjection.cs | 50 +++++++++++-- MapControl/Shared/WmsImageLayer.cs | 30 ++++---- MapControl/UWP/MapControl.UWP.csproj | 3 + MapControl/WPF/MapItemsImageLayer.WPF.cs | 94 ------------------------ 10 files changed, 145 insertions(+), 145 deletions(-) create mode 100644 MapControl/Shared/LatLonBox.cs delete mode 100644 MapControl/WPF/MapItemsImageLayer.WPF.cs diff --git a/MapControl/Shared/AzimuthalProjection.cs b/MapControl/Shared/AzimuthalProjection.cs index 430b04ca..65e44d15 100644 --- a/MapControl/Shared/AzimuthalProjection.cs +++ b/MapControl/Shared/AzimuthalProjection.cs @@ -21,7 +21,7 @@ namespace MapControl public override Rect? BoundingBoxToMap(BoundingBox boundingBox) { - Rect? mapRect = null; + Rect? rect = null; var center = LocationToMap(boundingBox.Center); if (center.HasValue) @@ -31,21 +31,21 @@ namespace MapControl var x = center.Value.X - width / 2d; var y = center.Value.Y - height / 2d; - mapRect = new Rect(x, y, width, height); + rect = new Rect(x, y, width, height); } - return mapRect; + return rect; } - public override BoundingBox MapToBoundingBox(Rect mapRect) + public override BoundingBox MapToBoundingBox(Rect rect) { BoundingBox boundingBox = null; - var rectCenter = new Point(mapRect.X + mapRect.Width / 2d, mapRect.Y + mapRect.Height / 2d); + var rectCenter = new Point(rect.X + rect.Width / 2d, rect.Y + rect.Height / 2d); var center = MapToLocation(rectCenter); if (center != null) { - boundingBox = new CenteredBoundingBox(center, mapRect.Width / Wgs84MeterPerDegree, mapRect.Height / Wgs84MeterPerDegree); + boundingBox = new CenteredBoundingBox(center, rect.Width / Wgs84MeterPerDegree, rect.Height / Wgs84MeterPerDegree); } return boundingBox; diff --git a/MapControl/Shared/BoundingBox.cs b/MapControl/Shared/BoundingBox.cs index eb90c0bd..fbde6955 100644 --- a/MapControl/Shared/BoundingBox.cs +++ b/MapControl/Shared/BoundingBox.cs @@ -21,13 +21,12 @@ namespace MapControl { } - public BoundingBox(double latitude1, double longitude1, double latitude2, double longitude2, double rotation = 0d) + public BoundingBox(double latitude1, double longitude1, double latitude2, double longitude2) { South = Math.Min(Math.Max(Math.Min(latitude1, latitude2), -90d), 90d); North = Math.Min(Math.Max(Math.Max(latitude1, latitude2), -90d), 90d); West = Math.Min(longitude1, longitude2); East = Math.Max(longitude1, longitude2); - Rotation = rotation; } public BoundingBox(Location location1, Location location2) @@ -39,7 +38,6 @@ namespace MapControl public double North { get; } public double West { get; } public double East { get; } - public double Rotation { get; } public virtual double Width => East - West; public virtual double Height => North - South; @@ -47,7 +45,7 @@ namespace MapControl public virtual Location Center => new Location((South + North) / 2d, (West + East) / 2d); /// - /// Creates a BoundingBox instance from a string containing a comma-separated sequence of four floating point numbers. + /// Creates a BoundingBox instance from a string containing a comma-separated sequence of four or five floating point numbers. /// public static BoundingBox Parse(string boundingBox) { @@ -67,7 +65,9 @@ namespace MapControl ? double.Parse(values[4], NumberStyles.Float, CultureInfo.InvariantCulture) : 0d; - return new BoundingBox( + // always return a LatLonBox, i.e. a BoundingBox with optional rotation, as used by GeoImage and GroundOverlay + // + return new LatLonBox( double.Parse(values[0], NumberStyles.Float, CultureInfo.InvariantCulture), double.Parse(values[1], NumberStyles.Float, CultureInfo.InvariantCulture), double.Parse(values[2], NumberStyles.Float, CultureInfo.InvariantCulture), diff --git a/MapControl/Shared/GeoImage.cs b/MapControl/Shared/GeoImage.cs index 49d146f5..58796499 100644 --- a/MapControl/Shared/GeoImage.cs +++ b/MapControl/Shared/GeoImage.cs @@ -85,7 +85,7 @@ namespace MapControl var boundingBox = geoImage.mapProjection != null ? geoImage.mapProjection.MapToBoundingBox(new Rect(p1, p2)) - : new BoundingBox(p1.Y, p1.X, p2.Y, p2.X); + : new LatLonBox(p1.Y, p1.X, p2.Y, p2.X, 0d); if (element is Image image) { @@ -94,6 +94,18 @@ namespace MapControl else if (element is Shape shape) { shape.Fill = geoImage.ImageBrush; +#if WPF + MapPanel.GetParentMap(shape).Children.Add(new MapPolygon + { + Stroke = Brushes.Green, + StrokeThickness = 1, + Locations = new LocationCollection( + new Location(boundingBox.North, boundingBox.West), + new Location(boundingBox.North, boundingBox.East), + new Location(boundingBox.South, boundingBox.East), + new Location(boundingBox.South, boundingBox.West)) + }); +#endif } MapPanel.SetBoundingBox(element, boundingBox); diff --git a/MapControl/Shared/GroundOverlay.cs b/MapControl/Shared/GroundOverlay.cs index 7720fb3f..4d3bd023 100644 --- a/MapControl/Shared/GroundOverlay.cs +++ b/MapControl/Shared/GroundOverlay.cs @@ -171,8 +171,8 @@ namespace MapControl { foreach (var groundOverlayElement in folderElement.Elements(ns + "GroundOverlay")) { - var boundingBoxElement = groundOverlayElement.Element(ns + "LatLonBox"); - var boundingBox = boundingBoxElement != null ? ReadBoundingBox(boundingBoxElement) : null; + var latLonBoxElement = groundOverlayElement.Element(ns + "LatLonBox"); + var latLonBox = latLonBoxElement != null ? ReadLatLonBox(latLonBoxElement) : null; var imagePathElement = groundOverlayElement.Element(ns + "Icon"); var imagePath = imagePathElement?.Element(ns + "href")?.Value; @@ -180,15 +180,15 @@ namespace MapControl var drawOrder = groundOverlayElement.Element(ns + "drawOrder")?.Value; var zIndex = drawOrder != null ? int.Parse(drawOrder) : 0; - if (boundingBox != null && imagePath != null) + if (latLonBox != null && imagePath != null) { - yield return new ImageOverlay(boundingBox, imagePath, zIndex); + yield return new ImageOverlay(latLonBox, imagePath, zIndex); } } } } - private static BoundingBox ReadBoundingBox(XElement latLonBoxElement) + private static LatLonBox ReadLatLonBox(XElement latLonBoxElement) { var ns = latLonBoxElement.Name.Namespace; var north = double.NaN; @@ -234,7 +234,7 @@ namespace MapControl throw new FormatException("Invalid LatLonBox"); } - return new BoundingBox(south, west, north, east, rotation); + return new LatLonBox(south, west, north, east, rotation); } } } diff --git a/MapControl/Shared/LatLonBox.cs b/MapControl/Shared/LatLonBox.cs new file mode 100644 index 00000000..a3756419 --- /dev/null +++ b/MapControl/Shared/LatLonBox.cs @@ -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) + +namespace MapControl +{ + /// + /// A BoundingBox with optional rotation. Used by GeoImage and GroundOverlay. + /// + public class LatLonBox : BoundingBox + { + public LatLonBox(double latitude1, double longitude1, double latitude2, double longitude2, double rotation) + : base(latitude1, longitude1, latitude2, longitude2) + { + Rotation = rotation; + } + + public LatLonBox(Location location1, Location location2, double rotation) + : base(location1, location2) + { + Rotation = rotation; + } + + /// + /// Gets a counterclockwise rotation angle in degrees. + /// + public double Rotation { get; } + } +} diff --git a/MapControl/Shared/MapPanel.cs b/MapControl/Shared/MapPanel.cs index 3c110094..226935a8 100644 --- a/MapControl/Shared/MapPanel.cs +++ b/MapControl/Shared/MapPanel.cs @@ -206,16 +206,28 @@ namespace MapControl return position; } - private Rect? GetViewRect(BoundingBox boundingBox) + private Tuple GetViewRect(BoundingBox boundingBox) { - var rect = parentMap.MapProjection.BoundingBoxToMap(boundingBox); - - if (rect.HasValue) + if (boundingBox is LatLonBox latLonBox) { - rect = GetViewRect(rect.Value); + var rotatedRect = parentMap.MapProjection.LatLonBoxToMap(latLonBox); + + if (rotatedRect != null) + { + return new Tuple(GetViewRect(rotatedRect.Item1), rotatedRect.Item2); + } + } + else + { + var mapRect = parentMap.MapProjection.BoundingBoxToMap(boundingBox); + + if (mapRect.HasValue) + { + return new Tuple(GetViewRect(mapRect.Value), 0d); + } } - return rect; + return null; } private Rect GetViewRect(Rect mapRect) @@ -287,16 +299,16 @@ namespace MapControl private void ArrangeElement(FrameworkElement element, BoundingBox boundingBox) { - var rect = GetViewRect(boundingBox); + var viewRect = GetViewRect(boundingBox); - if (rect.HasValue) + if (viewRect != null) { - element.Width = rect.Value.Width; - element.Height = rect.Value.Height; + element.Width = viewRect.Item1.Width; + element.Height = viewRect.Item1.Height; - element.Arrange(rect.Value); + element.Arrange(viewRect.Item1); - var rotation = parentMap.ViewTransform.Rotation - boundingBox.Rotation; + var rotation = parentMap.ViewTransform.Rotation - viewRect.Item2; if (element.RenderTransform is RotateTransform rotateTransform) { diff --git a/MapControl/Shared/MapProjection.cs b/MapControl/Shared/MapProjection.cs index 0b1f810d..95017679 100644 --- a/MapControl/Shared/MapProjection.cs +++ b/MapControl/Shared/MapProjection.cs @@ -66,27 +66,28 @@ namespace MapControl /// public virtual Rect? BoundingBoxToMap(BoundingBox boundingBox) { - Rect? mapRect = null; + Rect? rect = null; + var southWest = LocationToMap(new Location(boundingBox.South, boundingBox.West)); var northEast = LocationToMap(new Location(boundingBox.North, boundingBox.East)); if (southWest.HasValue && northEast.HasValue) { - mapRect = new Rect(southWest.Value, northEast.Value); + rect = new Rect(southWest.Value, northEast.Value); } - return mapRect; + return rect; } /// /// Transforms a Rect in projected map coordinates to a BoundingBox in geographic coordinates. /// Returns null when the MapRect can not be transformed. /// - public virtual BoundingBox MapToBoundingBox(Rect mapRect) + public virtual BoundingBox MapToBoundingBox(Rect rect) { BoundingBox boundingBox = null; - var southWest = MapToLocation(new Point(mapRect.X, mapRect.Y)); - var northEast = MapToLocation(new Point(mapRect.X + mapRect.Width, mapRect.Y + mapRect.Height)); + var southWest = MapToLocation(new Point(rect.X, rect.Y)); + var northEast = MapToLocation(new Point(rect.X + rect.Width, rect.Y + rect.Height)); if (southWest != null && northEast != null) { @@ -95,5 +96,42 @@ namespace MapControl return boundingBox; } + + /// + /// Transforms a LatLonBox in geographic coordinates to a rotated Rect in projected map coordinates. + /// Returns null when the LatLonBox can not be transformed. + /// + public virtual Tuple LatLonBoxToMap(LatLonBox latLonBox) + { + Tuple rotatedRect = null; + Point? center, north, south, west, east; + var boxCenter = latLonBox.Center; + + if ((center = LocationToMap(boxCenter)).HasValue && + (north = LocationToMap(new Location(latLonBox.North, boxCenter.Longitude))).HasValue && + (south = LocationToMap(new Location(latLonBox.South, boxCenter.Longitude))).HasValue && + (west = LocationToMap(new Location(boxCenter.Latitude, latLonBox.West))).HasValue && + (east = LocationToMap(new Location(boxCenter.Latitude, latLonBox.East))).HasValue) + { + var dx1 = east.Value.X - west.Value.X; + var dy1 = east.Value.Y - west.Value.Y; + var dx2 = north.Value.X - south.Value.X; + var dy2 = north.Value.Y - south.Value.Y; + var width = Math.Sqrt(dx1 * dx1 + dy1 * dy1); + var height = Math.Sqrt(dx2 * dx2 + dy2 * dy2); + var x = center.Value.X - width / 2d; + var y = center.Value.Y - height / 2d; + + // angles measured relative to horizontal and vertical axis + var r1 = (Math.Atan2(dy1, dx1) * 180d / Math.PI + 180d) % 360d - 180d; + var r2 = (Math.Atan2(-dx2, dy2) * 180d / Math.PI + 180d) % 360d - 180d; + + System.Diagnostics.Debug.WriteLine($"{r1}, {r2}"); + + rotatedRect = new Tuple(new Rect(x, y, width, height), latLonBox.Rotation + (r1 + r2) / 2d); + } + + return rotatedRect; + } } } diff --git a/MapControl/Shared/WmsImageLayer.cs b/MapControl/Shared/WmsImageLayer.cs index 535a8bee..2f7fdee3 100644 --- a/MapControl/Shared/WmsImageLayer.cs +++ b/MapControl/Shared/WmsImageLayer.cs @@ -232,12 +232,12 @@ namespace MapControl protected virtual string GetMapRequestUri(BoundingBox boundingBox) { string uri = null; - var mapRect = ParentMap.MapProjection.BoundingBoxToMap(boundingBox); + var bbox = ParentMap.MapProjection.BoundingBoxToMap(boundingBox); - if (mapRect.HasValue) + if (bbox.HasValue) { - var width = ParentMap.ViewTransform.Scale * mapRect.Value.Width; - var height = ParentMap.ViewTransform.Scale * mapRect.Value.Height; + var width = ParentMap.ViewTransform.Scale * bbox.Value.Width; + var height = ParentMap.ViewTransform.Scale * bbox.Value.Height; uri = GetRequestUri(new Dictionary { @@ -248,7 +248,7 @@ namespace MapControl { "STYLES", WmsStyles ?? "" }, { "FORMAT", "image/png" }, { "CRS", GetCrsValue() }, - { "BBOX", GetBboxValue(boundingBox, mapRect.Value) }, + { "BBOX", GetBboxValue(boundingBox, bbox.Value) }, { "WIDTH", Math.Round(width).ToString("F0") }, { "HEIGHT", Math.Round(height).ToString("F0") } }); @@ -263,12 +263,12 @@ namespace MapControl protected virtual string GetFeatureInfoRequestUri(BoundingBox boundingBox, Point position, string format) { string uri = null; - var mapRect = ParentMap.MapProjection.BoundingBoxToMap(boundingBox); + var bbox = ParentMap.MapProjection.BoundingBoxToMap(boundingBox); - if (mapRect.HasValue) + if (bbox.HasValue) { - var width = ParentMap.ViewTransform.Scale * mapRect.Value.Width; - var height = ParentMap.ViewTransform.Scale * mapRect.Value.Height; + var width = ParentMap.ViewTransform.Scale * bbox.Value.Width; + var height = ParentMap.ViewTransform.Scale * bbox.Value.Height; var transform = ViewTransform.CreateTransformMatrix( -ParentMap.ActualWidth / 2d, -ParentMap.ActualWidth / 2d, @@ -287,7 +287,7 @@ namespace MapControl { "FORMAT", "image/png" }, { "INFO_FORMAT", format }, { "CRS", GetCrsValue() }, - { "BBOX", GetBboxValue(boundingBox, mapRect.Value) }, + { "BBOX", GetBboxValue(boundingBox, bbox.Value) }, { "WIDTH", Math.Round(width).ToString("F0") }, { "HEIGHT", Math.Round(height).ToString("F0") }, { "I", Math.Round(imagePos.X).ToString("F0") }, @@ -315,7 +315,7 @@ namespace MapControl return crsId; } - protected virtual string GetBboxValue(BoundingBox boundingBox, Rect mapRect) + protected virtual string GetBboxValue(BoundingBox boundingBox, Rect mapBoundingBox) { var crsId = ParentMap.MapProjection.CrsId; string format; @@ -332,10 +332,10 @@ namespace MapControl else { format = "{0:F2},{1:F2},{2:F2},{3:F2}"; - x1 = mapRect.X; - y1 = mapRect.Y; - x2 = mapRect.X + mapRect.Width; - y2 = mapRect.Y + mapRect.Height; + x1 = mapBoundingBox.X; + y1 = mapBoundingBox.Y; + x2 = mapBoundingBox.X + mapBoundingBox.Width; + y2 = mapBoundingBox.Y + mapBoundingBox.Height; } return string.Format(CultureInfo.InvariantCulture, format, x1, y1, x2, y2); diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index b9d24f8f..2da96286 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -86,6 +86,9 @@ ImageLoader.cs + + LatLonBox.cs + Location.cs diff --git a/MapControl/WPF/MapItemsImageLayer.WPF.cs b/MapControl/WPF/MapItemsImageLayer.WPF.cs deleted file mode 100644 index 3cb887be..00000000 --- a/MapControl/WPF/MapItemsImageLayer.WPF.cs +++ /dev/null @@ -1,94 +0,0 @@ -// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control -// Copyright © 2024 Clemens Fischer -// Licensed under the Microsoft Public License (Ms-PL) - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Media; - -namespace MapControl -{ - public interface IMapDrawingItem - { - IEnumerable Locations { get; } - - Drawing GetDrawing(IList points, double scale, double rotation); - } - - public class MapItemsImageLayer : MapImageLayer - { - public static readonly DependencyProperty ItemsSourceProperty = - DependencyPropertyHelper.Register>(nameof(ItemsSource)); - - public IEnumerable ItemsSource - { - get => (IEnumerable)GetValue(ItemsSourceProperty); - set => SetValue(ItemsSourceProperty, value); - } - - protected override async Task GetImageAsync(BoundingBox boundingBox, IProgress progress) - { - ImageSource image = null; - var projection = ParentMap?.MapProjection; - var items = ItemsSource; - - if (projection != null && items != null) - { - var mapRect = projection.BoundingBoxToMap(boundingBox); - - if (mapRect.HasValue) - { - image = await Task.Run(() => GetImage(projection, mapRect.Value, items)); - } - } - - return image; - } - - private DrawingImage GetImage(MapProjection projection, Rect mapRect, IEnumerable items) - { - var scale = ParentMap.ViewTransform.Scale; - var rotation = ParentMap.ViewTransform.Rotation; - var drawings = new DrawingGroup(); - - foreach (var item in items) - { - var points = item.Locations - .Select(location => projection.LocationToMap(location)) - .Where(point => point.HasValue) - .Select(point => point.Value) - .ToList(); - - if (points.Any(point => mapRect.Contains(point))) - { - for (int i = 0; i < points.Count; i++) - { - points[i] = new Point( - scale * (points[i].X - mapRect.X), - scale * ((mapRect.Y + mapRect.Height) - points[i].Y)); - } - - drawings.Children.Add(item.GetDrawing(points, scale, rotation)); - } - } - - var drawingBrush = new DrawingBrush - { - Drawing = drawings, - ViewboxUnits = BrushMappingMode.Absolute, - Viewbox = new Rect(0, 0, scale * mapRect.Width, scale * mapRect.Height), - }; - - var drawing = new GeometryDrawing( - drawingBrush, null, new RectangleGeometry(drawingBrush.Viewbox)); - - var image = new DrawingImage(drawing); - image.Freeze(); - - return image; - } - } -}