diff --git a/MapControl/Shared/BoundingBox.cs b/MapControl/Shared/BoundingBox.cs index 27a153bd..42f1e66c 100644 --- a/MapControl/Shared/BoundingBox.cs +++ b/MapControl/Shared/BoundingBox.cs @@ -11,29 +11,17 @@ namespace MapControl #else [System.ComponentModel.TypeConverter(typeof(BoundingBoxConverter))] #endif - public class BoundingBox + public class BoundingBox(double latitude1, double longitude1, double latitude2, double longitude2, bool projectAxisAligned = false) { - protected BoundingBox() - { - } + 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); - 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); - } - - public BoundingBox(Location location1, Location location2) - : this(location1.Latitude, location1.Longitude, location2.Latitude, location2.Longitude) - { - } - - public double South { get; } - public double North { get; } - public double West { get; } - public double East { get; } + /// + /// Indicates whether a MapProjection projects the BoundingBox to an axis-aligned or skewed rectangle. + /// + public bool ProjectAxisAligned { get; } = projectAxisAligned; public override string ToString() { @@ -41,7 +29,7 @@ namespace MapControl } /// - /// Creates a BoundingBox instance from a string containing a comma-separated sequence of four or five floating point numbers. + /// Creates a BoundingBox instance from a string containing a comma-separated sequence of four floating point numbers. /// public static BoundingBox Parse(string boundingBox) { @@ -49,26 +37,19 @@ namespace MapControl if (!string.IsNullOrEmpty(boundingBox)) { - values = boundingBox.Split(new char[] { ',' }); + values = boundingBox.Split(','); } if (values == null || values.Length != 4 && values.Length != 5) { - throw new FormatException($"{nameof(BoundingBox)} string must contain a comma-separated sequence of four or five floating point numbers."); + throw new FormatException($"{nameof(BoundingBox)} string must contain a comma-separated sequence of four floating point numbers."); } - var rotation = values.Length == 5 - ? double.Parse(values[4], NumberStyles.Float, CultureInfo.InvariantCulture) - : 0d; - - // Always return a LatLonBox, i.e. a BoundingBox with optional rotation, as used by GeoImage and GroundOverlay. - // - return new LatLonBox( + return new BoundingBox( double.Parse(values[0], NumberStyles.Float, CultureInfo.InvariantCulture), double.Parse(values[1], NumberStyles.Float, CultureInfo.InvariantCulture), double.Parse(values[2], NumberStyles.Float, CultureInfo.InvariantCulture), - double.Parse(values[3], NumberStyles.Float, CultureInfo.InvariantCulture), - rotation); + double.Parse(values[3], NumberStyles.Float, CultureInfo.InvariantCulture)); } } } diff --git a/MapControl/Shared/GeoImage.cs b/MapControl/Shared/GeoImage.cs index 0057833d..5fda3faf 100644 --- a/MapControl/Shared/GeoImage.cs +++ b/MapControl/Shared/GeoImage.cs @@ -44,13 +44,13 @@ namespace MapControl var p2 = transform.Transform(new Point(bitmap.PixelWidth, bitmap.PixelHeight)); #endif BitmapSource = bitmap; - LatLonBox = projection != null - ? new LatLonBox(projection.MapToBoundingBox(new Rect(p1, p2))) - : new LatLonBox(p1.Y, p1.X, p2.Y, p2.X); + BoundingBox = projection != null + ? projection.MapToBoundingBox(new Rect(p1, p2)) + : new BoundingBox(p1.Y, p1.X, p2.Y, p2.X); } public BitmapSource BitmapSource { get; } - public LatLonBox LatLonBox { get; } + public BoundingBox BoundingBox { get; } } private const ushort ProjectedCRSGeoKey = 3072; @@ -124,7 +124,7 @@ namespace MapControl }; } - MapPanel.SetBoundingBox(element, geoBitmap.LatLonBox); + MapPanel.SetBoundingBox(element, geoBitmap.BoundingBox); } catch (Exception ex) { diff --git a/MapControl/Shared/GroundOverlay.cs b/MapControl/Shared/GroundOverlay.cs index f3150b2b..6a3e7156 100644 --- a/MapControl/Shared/GroundOverlay.cs +++ b/MapControl/Shared/GroundOverlay.cs @@ -30,7 +30,7 @@ namespace MapControl { private class ImageOverlay { - public ImageOverlay(string path, LatLonBox latLonBox, int zIndex) + public ImageOverlay(string path, BoundingBox latLonBox, int zIndex) { ImagePath = path; SetBoundingBox(Image, latLonBox); @@ -191,14 +191,13 @@ namespace MapControl return imageOverlays; } - private static LatLonBox ReadLatLonBox(XElement latLonBoxElement) + private static BoundingBox ReadLatLonBox(XElement latLonBoxElement) { var ns = latLonBoxElement.Name.Namespace; var north = double.NaN; var south = double.NaN; var east = double.NaN; var west = double.NaN; - var rotation = 0d; var value = latLonBoxElement.Element(ns + "north")?.Value; if (value != null) @@ -224,12 +223,6 @@ namespace MapControl west = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); } - value = latLonBoxElement.Element(ns + "rotation")?.Value; - if (value != null) - { - rotation = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); - } - if (double.IsNaN(north) || double.IsNaN(south) || double.IsNaN(east) || double.IsNaN(west) || north <= south || east <= west) @@ -237,7 +230,7 @@ namespace MapControl throw new FormatException("Invalid LatLonBox"); } - return new LatLonBox(south, west, north, east, rotation); + return new BoundingBox(south, west, north, east); } } } diff --git a/MapControl/Shared/LatLonBox.cs b/MapControl/Shared/LatLonBox.cs deleted file mode 100644 index 8a26580a..00000000 --- a/MapControl/Shared/LatLonBox.cs +++ /dev/null @@ -1,31 +0,0 @@ -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 = 0d) - : base(latitude1, longitude1, latitude2, longitude2) - { - Rotation = rotation; - } - - public LatLonBox(Location location1, Location location2, double rotation = 0d) - : base(location1, location2) - { - Rotation = rotation; - } - - public LatLonBox(BoundingBox boundingBox, double rotation = 0d) - : base(boundingBox.South, boundingBox.West, boundingBox.North, boundingBox.East) - { - Rotation = rotation; - } - - /// - /// Gets a counterclockwise rotation angle in degrees. - /// - public double Rotation { get; } - } -} diff --git a/MapControl/Shared/Location.cs b/MapControl/Shared/Location.cs index 1c0e3d1f..1c7a3108 100644 --- a/MapControl/Shared/Location.cs +++ b/MapControl/Shared/Location.cs @@ -58,7 +58,7 @@ namespace MapControl if (!string.IsNullOrEmpty(location)) { - values = location.Split([',']); + values = location.Split(','); } if (values?.Length != 2) diff --git a/MapControl/Shared/LocationCollection.cs b/MapControl/Shared/LocationCollection.cs index 3a2f95f2..363ff3c3 100644 --- a/MapControl/Shared/LocationCollection.cs +++ b/MapControl/Shared/LocationCollection.cs @@ -62,7 +62,7 @@ namespace MapControl return string.IsNullOrEmpty(locations) ? new LocationCollection() : new LocationCollection(locations - .Split(new char[] { ' ', ';' }, StringSplitOptions.RemoveEmptyEntries) + .Split([' ', ';'], StringSplitOptions.RemoveEmptyEntries) .Select(Location.Parse)); } diff --git a/MapControl/Shared/MapBase.cs b/MapControl/Shared/MapBase.cs index f3d77bc9..c45be0ca 100644 --- a/MapControl/Shared/MapBase.cs +++ b/MapControl/Shared/MapBase.cs @@ -342,7 +342,7 @@ namespace MapControl /// public void ZoomToBounds(BoundingBox bounds) { - var mapRect = MapProjection.BoundingBoxToMap(bounds); + (var mapRect, var _) = MapProjection.BoundingBoxToMap(bounds); if (mapRect.HasValue) { diff --git a/MapControl/Shared/MapImageLayer.cs b/MapControl/Shared/MapImageLayer.cs index 91475c8e..a1c37032 100644 --- a/MapControl/Shared/MapImageLayer.cs +++ b/MapControl/Shared/MapImageLayer.cs @@ -193,7 +193,7 @@ 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); + boundingBox = ParentMap.MapProjection.MapToBoundingBox(mapRect, true); if (boundingBox != null) { diff --git a/MapControl/Shared/MapPanel.cs b/MapControl/Shared/MapPanel.cs index 484eae3a..e1d7e9ef 100644 --- a/MapControl/Shared/MapPanel.cs +++ b/MapControl/Shared/MapPanel.cs @@ -286,17 +286,7 @@ namespace MapControl private void ArrangeElement(FrameworkElement element, BoundingBox boundingBox) { - Rect? mapRect; - Matrix? transform = null; - - if (boundingBox is LatLonBox latLonBox) - { - (mapRect, transform) = parentMap.MapProjection.LatLonBoxToMap(latLonBox); - } - else - { - mapRect = parentMap.MapProjection.BoundingBoxToMap(boundingBox); - } + (var mapRect, var transform) = parentMap.MapProjection.BoundingBoxToMap(boundingBox); if (mapRect.HasValue) { diff --git a/MapControl/Shared/MapProjection.cs b/MapControl/Shared/MapProjection.cs index 06780f6a..fcdb89f2 100644 --- a/MapControl/Shared/MapProjection.cs +++ b/MapControl/Shared/MapProjection.cs @@ -138,71 +138,69 @@ 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. - /// Returns 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 when the BoundingBox is not projected axis-aligned. + /// Returns (null, null) when the BoundingBox can not be transformed. /// - public Rect? BoundingBoxToMap(BoundingBox boundingBox) + public (Rect?, Matrix?) BoundingBoxToMap(BoundingBox boundingBox) { + Rect? rect = null; + Matrix? transform = null; var sw = LocationToMap(boundingBox.South, boundingBox.West); var ne = LocationToMap(boundingBox.North, boundingBox.East); - return sw.HasValue && ne.HasValue ? new Rect(sw.Value, ne.Value) : null; + if (sw.HasValue && ne.HasValue) + { + if (boundingBox.ProjectAxisAligned) + { + 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); + } + } + } + } + + return (rect, transform); } /// /// 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) + public BoundingBox MapToBoundingBox(Rect rect, bool axisAligned = false) { 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, ne) : null; - } - - /// - /// Transforms a LatLonBox in geographic coordinates to a rotated Rect in projected map coordinates. - /// Returns (null, null) when the LatLonBox can not be transformed. - /// - public (Rect?, Matrix?) LatLonBoxToMap(LatLonBox latLonBox) - { - Rect? rect = null; - Matrix? transform = null; - var sw = LocationToMap(latLonBox.South, latLonBox.West); - var se = LocationToMap(latLonBox.South, latLonBox.East); - var nw = LocationToMap(latLonBox.North, latLonBox.West); - var ne = LocationToMap(latLonBox.North, latLonBox.East); - - if (sw.HasValue && se.HasValue && nw.HasValue && ne.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 - var x = center.X - width / 2d; - var y = center.Y - height / 2d; - - rect = new Rect(x, y, width, height); - - if (dy1 != 0d || dx2 != 0d || latLonBox.Rotation != 0d) - { - // Skew matrix with skewX = Atan(-dx2 / dy2) and skewY = Atan(-dy1 / dx1). - // - var t = new Matrix(1d, -dy1 / dx1, -dx2 / dy2, 1d, 0d, 0d); - t.Rotate(-latLonBox.Rotation); - transform = t; - } - } - - return (rect, transform); + return sw != null && ne != null + ? new BoundingBox(sw.Latitude, sw.Longitude, ne.Latitude, ne.Longitude, axisAligned) + : null; } public override string ToString()