From a07948be024f928646276e4b97f6ccc190ac2199 Mon Sep 17 00:00:00 2001 From: Clemens Date: Fri, 4 Mar 2022 22:28:18 +0100 Subject: [PATCH] General map graticule for WPF --- MapControl/Shared/AzimuthalProjection.cs | 2 +- MapControl/Shared/MapBase.cs | 91 ++++--- MapControl/Shared/MapGraticule.cs | 23 +- MapControl/Shared/MapPanel.cs | 8 +- MapControl/Shared/MapProjection.cs | 26 +- MapControl/Shared/OrthographicProjection.cs | 7 +- MapControl/WPF/MapGraticule.WPF.cs | 251 +++++++++++++++++-- MapProjections/Shared/GeoApiProjection.cs | 2 +- SampleApps/UniversalApp/MainPage.xaml.cs | 43 ++-- SampleApps/WinUiApp/MainWindow.xaml.cs | 43 ++-- SampleApps/WpfApplication/MainWindow.xaml.cs | 40 +-- 11 files changed, 414 insertions(+), 122 deletions(-) diff --git a/MapControl/Shared/AzimuthalProjection.cs b/MapControl/Shared/AzimuthalProjection.cs index 4ec31b6d..936fcb1d 100644 --- a/MapControl/Shared/AzimuthalProjection.cs +++ b/MapControl/Shared/AzimuthalProjection.cs @@ -31,7 +31,7 @@ namespace MapControl var width = rect.Width / Wgs84MeterPerDegree; var height = rect.Height / Wgs84MeterPerDegree; - return new CenteredBoundingBox(center, width, height); + return center != null ? new CenteredBoundingBox(center, width, height) : null; } /// diff --git a/MapControl/Shared/MapBase.cs b/MapControl/Shared/MapBase.cs index 887e59c5..c08f3608 100644 --- a/MapControl/Shared/MapBase.cs +++ b/MapControl/Shared/MapBase.cs @@ -279,7 +279,7 @@ namespace MapControl public void SetTransformCenter(Point center) { transformCenter = ViewToLocation(center); - viewCenter = center; + viewCenter = transformCenter != null ? center : new Point(RenderSize.Width / 2d, RenderSize.Height / 2d); } /// @@ -304,7 +304,11 @@ namespace MapControl if (translation.X != 0d || translation.Y != 0d) { - Center = ViewToLocation(viewCenter - translation); + var center = ViewToLocation(viewCenter - translation); + if (center != null) + { + Center = center; + } } } @@ -317,8 +321,8 @@ namespace MapControl { if (rotation != 0d || scale != 1d) { - transformCenter = ViewToLocation(center); - viewCenter = center + translation; + SetTransformCenter(center); + viewCenter += translation; if (rotation != 0d) { @@ -367,11 +371,16 @@ namespace MapControl { var rect = MapProjection.BoundingBoxToRect(boundingBox); var center = new Point(rect.X + rect.Width / 2d, rect.Y + rect.Height / 2d); - var scale = Math.Min(RenderSize.Width / rect.Width, RenderSize.Height / rect.Height); + var targetCenter = MapProjection.MapToLocation(center); - TargetZoomLevel = ViewTransform.ScaleToZoomLevel(scale); - TargetCenter = MapProjection.MapToLocation(center); - TargetHeading = 0d; + if (targetCenter != null) + { + var scale = Math.Min(RenderSize.Width / rect.Width, RenderSize.Height / rect.Height); + + TargetZoomLevel = ViewTransform.ScaleToZoomLevel(scale); + TargetCenter = targetCenter; + TargetHeading = 0d; + } } internal double ConstrainedLongitude(double longitude) @@ -700,46 +709,58 @@ namespace MapControl private void UpdateTransform(bool resetTransformCenter = false, bool projectionChanged = false) { var viewScale = ViewTransform.ZoomLevelToScale(ZoomLevel); - var center = transformCenter ?? Center; var projection = MapProjection; projection.Center = ProjectionCenter ?? Center; - ViewTransform.SetTransform(projection.LocationToMap(center), viewCenter, viewScale, Heading); + var mapCenter = projection.LocationToMap(transformCenter ?? Center); - if (transformCenter != null) + if (MapProjection.IsValid(mapCenter)) { - center = ViewToLocation(new Point(RenderSize.Width / 2d, RenderSize.Height / 2d)); - center.Longitude = Location.NormalizeLongitude(center.Longitude); + ViewTransform.SetTransform(mapCenter, viewCenter, viewScale, Heading); - if (center.Latitude < -projection.MaxLatitude || center.Latitude > projection.MaxLatitude) + if (transformCenter != null) { - center.Latitude = Math.Min(Math.Max(center.Latitude, -projection.MaxLatitude), projection.MaxLatitude); - resetTransformCenter = true; + var center = ViewToLocation(new Point(RenderSize.Width / 2d, RenderSize.Height / 2d)); + + if (center != null) + { + center.Longitude = Location.NormalizeLongitude(center.Longitude); + + if (center.Latitude < -projection.MaxLatitude || center.Latitude > projection.MaxLatitude) + { + center.Latitude = Math.Min(Math.Max(center.Latitude, -projection.MaxLatitude), projection.MaxLatitude); + resetTransformCenter = true; + } + + SetValueInternal(CenterProperty, center); + + if (centerAnimation == null) + { + SetValueInternal(TargetCenterProperty, center); + } + + if (resetTransformCenter) + { + ResetTransformCenter(); + + projection.Center = ProjectionCenter ?? center; + mapCenter = projection.LocationToMap(center); + + if (MapProjection.IsValid(mapCenter)) + { + ViewTransform.SetTransform(mapCenter, viewCenter, viewScale, Heading); + } + } + } } - SetValueInternal(CenterProperty, center); + SetViewScale(ViewTransform.Scale); - if (centerAnimation == null) - { - SetValueInternal(TargetCenterProperty, center); - } + OnViewportChanged(new ViewportChangedEventArgs(projectionChanged, Center.Longitude - centerLongitude)); - if (resetTransformCenter) - { - ResetTransformCenter(); - - projection.Center = ProjectionCenter ?? center; - - ViewTransform.SetTransform(projection.LocationToMap(center), viewCenter, viewScale, Heading); - } + centerLongitude = Center.Longitude; } - - SetViewScale(ViewTransform.Scale); - - OnViewportChanged(new ViewportChangedEventArgs(projectionChanged, Center.Longitude - centerLongitude)); - - centerLongitude = Center.Longitude; } protected override void OnViewportChanged(ViewportChangedEventArgs e) diff --git a/MapControl/Shared/MapGraticule.cs b/MapControl/Shared/MapGraticule.cs index 0cb343e8..6ef6c2a5 100644 --- a/MapControl/Shared/MapGraticule.cs +++ b/MapControl/Shared/MapGraticule.cs @@ -33,15 +33,9 @@ namespace MapControl private double GetLineDistance() { - var pixelPerDegree = ParentMap.ViewTransform.Scale * MapProjection.Wgs84MeterPerDegree; - var minDistance = MinLineDistance / pixelPerDegree; - var scale = 1d; - - if (minDistance < 1d) - { - scale = minDistance < 1d / 60d ? 3600d : 60d; - minDistance *= scale; - } + var minDistance = MinLineDistance / PixelPerLongitudeDegree(ParentMap.Center); + var scale = minDistance < 1d / 60d ? 3600d : minDistance < 1d ? 60d : 1d; + minDistance *= scale; var lineDistances = new double[] { 1d, 2d, 5d, 10d, 15d, 30d, 60d }; var i = 0; @@ -51,7 +45,14 @@ namespace MapControl i++; } - return lineDistances[i] / scale; + return Math.Min(lineDistances[i] / scale, 30d); + } + + private double PixelPerLongitudeDegree(Location location) + { + return Math.Max(1d, // a reasonable lower limit + ParentMap.GetScale(location).X * + Math.Cos(location.Latitude * Math.PI / 180d) * MapProjection.Wgs84MeterPerDegree); } private static string GetLabelFormat(double lineDistance) @@ -64,6 +65,8 @@ namespace MapControl { var hemisphere = hemispheres[0]; + value = (value + 540d) % 360d - 180d; + if (value < -1e-8) // ~1mm { value = -value; diff --git a/MapControl/Shared/MapPanel.cs b/MapControl/Shared/MapPanel.cs index c2f59d29..e88dc7a2 100644 --- a/MapControl/Shared/MapPanel.cs +++ b/MapControl/Shared/MapPanel.cs @@ -147,9 +147,11 @@ namespace MapControl if (parentMap.MapProjection.IsNormalCylindrical && IsOutsideViewport(position)) { var location = parentMap.MapProjection.MapToLocation(center); - location.Longitude = parentMap.ConstrainedLongitude(location.Longitude); - - position = parentMap.LocationToView(location); + if (location != null) + { + location.Longitude = parentMap.ConstrainedLongitude(location.Longitude); + position = parentMap.LocationToView(location); + } } var width = rect.Width * parentMap.ViewTransform.Scale; diff --git a/MapControl/Shared/MapProjection.cs b/MapControl/Shared/MapProjection.cs index 03060eaa..7c742a31 100644 --- a/MapControl/Shared/MapProjection.cs +++ b/MapControl/Shared/MapProjection.cs @@ -64,6 +64,7 @@ namespace MapControl /// /// Transforms a Point in cartesian map coordinates to a Location in geographic coordinates. + /// Returns null when the Point can not be transformed. /// public abstract Location MapToLocation(Point point); @@ -79,13 +80,16 @@ namespace MapControl /// /// Transforms a Rect in cartesian map coordinates to a BoundingBox in geographic coordinates. + /// Returns null when the Rect can not be transformed. /// public virtual BoundingBox RectToBoundingBox(Rect rect) { var sw = MapToLocation(new Point(rect.X, rect.Y)); var ne = MapToLocation(new Point(rect.X + rect.Width, rect.Y + rect.Height)); - return new BoundingBox(sw.Latitude, sw.Longitude, ne.Latitude, ne.Longitude); + return sw != null && ne != null + ? new BoundingBox(sw.Latitude, sw.Longitude, ne.Latitude, ne.Longitude) + : null; } /// @@ -106,5 +110,25 @@ namespace MapControl return string.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3}", rect.X, rect.Y, (rect.X + rect.Width), (rect.Y + rect.Height)); } + + /// + /// Checks if the X and Y values of a Point are neither NaN nor Infinity. + /// + public static bool IsValid(Point point) + { + return !double.IsNaN(point.X) && !double.IsInfinity(point.X) && + !double.IsNaN(point.Y) && !double.IsInfinity(point.Y); + } + + /// + /// Checks if the X, Y, Width and Height values of a Rect are neither NaN nor Infinity. + /// + public static bool IsValid(Rect rect) + { + return !double.IsNaN(rect.X) && !double.IsInfinity(rect.X) && + !double.IsNaN(rect.Y) && !double.IsInfinity(rect.Y) && + !double.IsNaN(rect.Width) && !double.IsInfinity(rect.Width) && + !double.IsNaN(rect.Height) && !double.IsInfinity(rect.Height); + } } } diff --git a/MapControl/Shared/OrthographicProjection.cs b/MapControl/Shared/OrthographicProjection.cs index ca1b46ec..32b8ff15 100644 --- a/MapControl/Shared/OrthographicProjection.cs +++ b/MapControl/Shared/OrthographicProjection.cs @@ -33,6 +33,11 @@ namespace MapControl var lat = location.Latitude * Math.PI / 180d; var dLon = (location.Longitude - Center.Longitude) * Math.PI / 180d; + if (Math.Abs(lat - lat0) > Math.PI / 2d || Math.Abs(dLon) > Math.PI / 2d) + { + return new Point(double.NaN, double.NaN); + } + return new Point( Wgs84EquatorialRadius * Math.Cos(lat) * Math.Sin(dLon), Wgs84EquatorialRadius * (Math.Cos(lat0) * Math.Sin(lat) - Math.Sin(lat0) * Math.Cos(lat) * Math.Cos(dLon))); @@ -51,7 +56,7 @@ namespace MapControl if (r2 > 1d) { - return new Location(double.NaN, double.NaN); + return null; } var r = Math.Sqrt(r2); diff --git a/MapControl/WPF/MapGraticule.WPF.cs b/MapControl/WPF/MapGraticule.WPF.cs index d52326e3..465e3e65 100644 --- a/MapControl/WPF/MapGraticule.WPF.cs +++ b/MapControl/WPF/MapGraticule.WPF.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Windows; using System.Windows.Media; @@ -12,6 +13,8 @@ namespace MapControl { public partial class MapGraticule { + private const double LineInterpolationResolution = 2d; + private class Label { public readonly double Position; @@ -40,29 +43,30 @@ namespace MapControl if (projection != null) { - var lineDistance = GetLineDistance(); - var labelFormat = GetLabelFormat(lineDistance); - if (projection.IsNormalCylindrical) { - DrawCylindricalGraticule(drawingContext, lineDistance, labelFormat); + DrawCylindricalGraticule(drawingContext); } else { + DrawGraticule(drawingContext); } } } - private void DrawCylindricalGraticule(DrawingContext drawingContext, double lineDistance, string labelFormat) + private void DrawCylindricalGraticule(DrawingContext drawingContext) { + var path = new PathGeometry(); + var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); + var pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip; + var lineDistance = GetLineDistance(); + var labelFormat = GetLabelFormat(lineDistance); + var boundingBox = ParentMap.ViewRectToBoundingBox(new Rect(ParentMap.RenderSize)); var latLabelStart = Math.Ceiling(boundingBox.South / lineDistance) * lineDistance; var lonLabelStart = Math.Ceiling(boundingBox.West / lineDistance) * lineDistance; var latLabels = new List