From ab155a26e73fa8c4c28969a01879939cad483926 Mon Sep 17 00:00:00 2001 From: ClemensFischer Date: Tue, 20 Jan 2026 09:48:16 +0100 Subject: [PATCH] Use Matrix for projection relative scale --- .../Shared/AutoEquirectangularProjection.cs | 7 ++-- .../Shared/AzimuthalEquidistantProjection.cs | 5 +-- .../Shared/EquirectangularProjection.cs | 5 +-- MapControl/Shared/GnomonicProjection.cs | 11 ++++--- MapControl/Shared/MapBase.cs | 32 ++++++------------- MapControl/Shared/MapGraticule.cs | 14 +++----- MapControl/Shared/MapItem.cs | 2 +- MapControl/Shared/MapPath.cs | 2 +- MapControl/Shared/MapProjection.cs | 8 +++-- MapControl/Shared/MapScale.cs | 6 ++-- MapControl/Shared/Matrix.cs | 25 +++++++++++---- MapControl/Shared/OrthographicProjection.cs | 6 ++-- .../Shared/PolarStereographicProjection.cs | 15 ++++----- MapControl/Shared/StereographicProjection.cs | 5 +-- .../Shared/TransverseMercatorProjection.cs | 5 --- MapControl/Shared/WebMercatorProjection.cs | 5 +-- MapControl/Shared/WorldMercatorProjection.cs | 5 +-- MapProjections/Shared/ProjNetMapProjection.cs | 7 ---- .../Shared/WebMercatorProjection.cs | 8 ++--- MapProjections/Shared/Wgs84UpsProjections.cs | 16 ++++------ .../Shared/WorldMercatorProjection.cs | 8 ++--- 21 files changed, 93 insertions(+), 104 deletions(-) diff --git a/MapControl/Shared/AutoEquirectangularProjection.cs b/MapControl/Shared/AutoEquirectangularProjection.cs index c4b36924..c075715d 100644 --- a/MapControl/Shared/AutoEquirectangularProjection.cs +++ b/MapControl/Shared/AutoEquirectangularProjection.cs @@ -1,6 +1,7 @@ using System; #if WPF using System.Windows; +using System.Windows.Media; #elif AVALONIA using Avalonia; #endif @@ -27,11 +28,11 @@ namespace MapControl CrsId = crsId; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { - return new Point( + return new Matrix( Math.Cos(Center.Latitude * Math.PI / 180d) / Math.Cos(latitude * Math.PI / 180d), - 1d); + 0d, 0d, 1d, 0d, 0d); } public override Point? LocationToMap(double latitude, double longitude) diff --git a/MapControl/Shared/AzimuthalEquidistantProjection.cs b/MapControl/Shared/AzimuthalEquidistantProjection.cs index 8f1fc91a..8e522514 100644 --- a/MapControl/Shared/AzimuthalEquidistantProjection.cs +++ b/MapControl/Shared/AzimuthalEquidistantProjection.cs @@ -1,6 +1,7 @@ using System; #if WPF using System.Windows; +using System.Windows.Media; #elif AVALONIA using Avalonia; #endif @@ -25,7 +26,7 @@ namespace MapControl CrsId = crsId; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { (var cosC, var _, var _) = GetPointValues(latitude, longitude); var k = 1d; @@ -36,7 +37,7 @@ namespace MapControl k = c / Math.Sin(c); // p.195 (25-2) } - return new Point(k, k); + return new Matrix(k, 0d, 0d, k, 0d, 0d); } public override Point? LocationToMap(double latitude, double longitude) diff --git a/MapControl/Shared/EquirectangularProjection.cs b/MapControl/Shared/EquirectangularProjection.cs index a35116e9..b2832c3b 100644 --- a/MapControl/Shared/EquirectangularProjection.cs +++ b/MapControl/Shared/EquirectangularProjection.cs @@ -1,6 +1,7 @@ using System; #if WPF using System.Windows; +using System.Windows.Media; #elif AVALONIA using Avalonia; #endif @@ -27,9 +28,9 @@ namespace MapControl CrsId = crsId; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { - return new Point(1d / Math.Cos(latitude * Math.PI / 180d), 1d); + return new Matrix(1d / Math.Cos(latitude * Math.PI / 180d), 0d, 0d, 1d, 0d, 0d); } public override Point? LocationToMap(double latitude, double longitude) diff --git a/MapControl/Shared/GnomonicProjection.cs b/MapControl/Shared/GnomonicProjection.cs index 46399eb7..d633eabf 100644 --- a/MapControl/Shared/GnomonicProjection.cs +++ b/MapControl/Shared/GnomonicProjection.cs @@ -1,6 +1,7 @@ using System; #if WPF using System.Windows; +using System.Windows.Media; #elif AVALONIA using Avalonia; #endif @@ -25,12 +26,14 @@ namespace MapControl CrsId = crsId; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { - (var cosC, var _, var _) = GetPointValues(latitude, longitude); - var h = 1d / (cosC * cosC); // p.165 (22-2) + (var cosC, var x, var y) = GetPointValues(latitude, longitude); + var k = 1d / cosC; // p.165 (22-3) + var h = k * k; // p.165 (22-2) - return new Point(h, h); // TODO: rotate + var scale = new Matrix(h, 0d, 0d, h, 0d, 0d); + return scale; } public override Point? LocationToMap(double latitude, double longitude) diff --git a/MapControl/Shared/MapBase.cs b/MapControl/Shared/MapBase.cs index 36c790bd..58e4ef42 100644 --- a/MapControl/Shared/MapBase.cs +++ b/MapControl/Shared/MapBase.cs @@ -177,37 +177,25 @@ namespace MapControl public ViewTransform ViewTransform { get; } = new ViewTransform(); /// - /// Gets the map scale as horizontal and vertical scaling factors from meters to - /// view coordinates at the specified geographic coordinates. + /// Gets a transform Matrix from meters to view coordinates for scaling and rotating + /// at the specified geographic coordinates. /// - public Point GetMapScale(double latitude, double longitude) + public Matrix GetMapToViewTransform(double latitude, double longitude) { - var relativeScale = MapProjection.RelativeScale(latitude, longitude); + var transform = MapProjection.RelativeScale(latitude, longitude); + transform.Scale(ViewTransform.Scale, ViewTransform.Scale); + transform.Rotate(ViewTransform.Rotation); - return new Point(ViewTransform.Scale * relativeScale.X, ViewTransform.Scale * relativeScale.Y); - } - - /// - /// Gets the map scale as horizontal and vertical scaling factors from meters to - /// view coordinates at the specified location. - /// - public Point GetMapScale(Location location) - { - return GetMapScale(location.Latitude, location.Longitude); + return transform; } /// /// Gets a transform Matrix from meters to view coordinates for scaling and rotating - /// objects that are anchored at the specified Location. + /// at the specified Location. /// - public Matrix GetMapTransform(Location location) + public Matrix GetMapToViewTransform(Location location) { - var mapScale = GetMapScale(location.Latitude, location.Longitude); - var transform = new Matrix(mapScale.X, 0d, 0d, mapScale.Y, 0d, 0d); - - transform.Rotate(ViewTransform.Rotation); - - return transform; + return GetMapToViewTransform(location.Latitude, location.Longitude); } /// diff --git a/MapControl/Shared/MapGraticule.cs b/MapControl/Shared/MapGraticule.cs index 3be09c4d..86a3e990 100644 --- a/MapControl/Shared/MapGraticule.cs +++ b/MapControl/Shared/MapGraticule.cs @@ -75,12 +75,14 @@ namespace MapControl set => SetValue(FontSizeProperty, value); } + private double PixelPerDegree => Math.Max(1d, ParentMap.ViewTransform.Scale * MapProjection.Wgs84MeterPerDegree); + private double lineDistance; private string labelFormat; private void SetLineDistance() { - var minDistance = MinLineDistance / PixelPerLongitudeDegree(ParentMap.Center.Latitude, ParentMap.Center.Longitude); + var minDistance = MinLineDistance / PixelPerDegree; var scale = minDistance < 1d / 60d ? 3600d : minDistance < 1d ? 60d : 1d; minDistance *= scale; @@ -98,14 +100,6 @@ namespace MapControl : lineDistance < 1d ? "{0} {1}°{2:00}'" : "{0} {1}°"; } - private double PixelPerLongitudeDegree(double latitude, double longitude) - { - var scale = ParentMap.GetMapScale(latitude, longitude); - - return Math.Max(1d, // a reasonable lower limit - scale.X * Math.Cos(latitude * Math.PI / 180d) * MapProjection.Wgs84MeterPerDegree); - } - private string GetLabelText(double value, string hemispheres) { var hemisphere = hemispheres[0]; @@ -131,7 +125,7 @@ namespace MapControl { // Get rotation from second location with same latitude. // - var pos = ParentMap.LocationToView(latitude, longitude + 10d / PixelPerLongitudeDegree(latitude, longitude)); + var pos = ParentMap.LocationToView(latitude, longitude + 10d / PixelPerDegree); if (pos.HasValue) { diff --git a/MapControl/Shared/MapItem.cs b/MapControl/Shared/MapItem.cs index eff1c54b..16de22f5 100644 --- a/MapControl/Shared/MapItem.cs +++ b/MapControl/Shared/MapItem.cs @@ -96,7 +96,7 @@ namespace MapControl { if (MapTransform != null && ParentMap != null && Location != null) { - MapTransform.Matrix = ParentMap.GetMapTransform(Location); + MapTransform.Matrix = ParentMap.GetMapToViewTransform(Location); } } } diff --git a/MapControl/Shared/MapPath.cs b/MapControl/Shared/MapPath.cs index 7e10ef8b..ed8fca86 100644 --- a/MapControl/Shared/MapPath.cs +++ b/MapControl/Shared/MapPath.cs @@ -66,7 +66,7 @@ namespace MapControl { if (ParentMap != null && Location != null && Data != null) { - SetDataTransform(ParentMap.GetMapTransform(Location)); + SetDataTransform(ParentMap.GetMapToViewTransform(Location)); } MapPanel.SetLocation(this, Location); diff --git a/MapControl/Shared/MapProjection.cs b/MapControl/Shared/MapProjection.cs index 288f513a..8657486f 100644 --- a/MapControl/Shared/MapProjection.cs +++ b/MapControl/Shared/MapProjection.cs @@ -1,6 +1,7 @@ using System; #if WPF using System.Windows; +using System.Windows.Media; #elif AVALONIA using Avalonia; #endif @@ -82,9 +83,10 @@ namespace MapControl } /// - /// Gets the relative map scale at the specified geographic coordinates. + /// Gets the relative scale at the specified geographic coordinates. + /// The returned Matrix represents the local distortion of the map projection. /// - public virtual Point RelativeScale(double latitude, double longitude) => new Point(1d, 1d); + public virtual Matrix RelativeScale(double latitude, double longitude) => new Matrix(1d, 0d, 0d, 1d, 0d, 0d); /// /// Transforms geographic coordinates to a Point in projected map coordinates. @@ -101,7 +103,7 @@ namespace MapControl /// /// Gets the relative map scale at the specified geographic Location. /// - public Point RelativeScale(Location location) => RelativeScale(location.Latitude, location.Longitude); + public Matrix RelativeScale(Location location) => RelativeScale(location.Latitude, location.Longitude); /// /// Transforms a Location in geographic coordinates to a Point in projected map coordinates. diff --git a/MapControl/Shared/MapScale.cs b/MapControl/Shared/MapScale.cs index 6b3a7baf..572a28e3 100644 --- a/MapControl/Shared/MapScale.cs +++ b/MapControl/Shared/MapScale.cs @@ -88,13 +88,13 @@ namespace MapControl protected override Size MeasureOverride(Size availableSize) { - double scale; - - if (ParentMap == null || (scale = ParentMap.GetMapScale(ParentMap.Center).X) <= 0d) + if (ParentMap == null) { return new Size(); } + var p = ParentMap.GetMapToViewTransform(ParentMap.Center).Transform(new Point(1d, 0d)); + var scale = Math.Sqrt(p.X * p.X + p.Y * p.Y); var length = MinWidth / scale; var magnitude = Math.Pow(10d, Math.Floor(Math.Log10(length))); diff --git a/MapControl/Shared/Matrix.cs b/MapControl/Shared/Matrix.cs index 030a55e6..aa5b4355 100644 --- a/MapControl/Shared/Matrix.cs +++ b/MapControl/Shared/Matrix.cs @@ -50,6 +50,19 @@ namespace MapControl OffsetY += y; } + public void Scale(double scaleX, double scaleY) + { + // equivalent to Multiply(new Matrix(scaleX, 0, 0, scaleY, 0d, 0d)); + // + SetMatrix( + M11 * scaleX, + M12 * scaleY, + M21 * scaleX, + M22 * scaleY, + OffsetX * scaleX, + OffsetY * scaleY); + } + public void Rotate(double angle) { if (angle != 0d) @@ -60,12 +73,12 @@ namespace MapControl // equivalent to Multiply(new Matrix(cos, sin, -sin, cos, 0d, 0d)); // SetMatrix( - cos * M11 - sin * M12, - sin * M11 + cos * M12, - cos * M21 - sin * M22, - sin * M21 + cos * M22, - cos * OffsetX - sin * OffsetY, - sin * OffsetX + cos * OffsetY); + M11 * cos - M12 * sin, + M11 * sin + M12 * cos, + M21 * cos - M22 * sin, + M21 * sin + M22 * cos, + OffsetX * cos - OffsetY * sin, + OffsetX * sin + OffsetY * cos); } } diff --git a/MapControl/Shared/OrthographicProjection.cs b/MapControl/Shared/OrthographicProjection.cs index 10181779..db74fd8e 100644 --- a/MapControl/Shared/OrthographicProjection.cs +++ b/MapControl/Shared/OrthographicProjection.cs @@ -1,6 +1,7 @@ using System; #if WPF using System.Windows; +using System.Windows.Media; #elif AVALONIA using Avalonia; #endif @@ -25,12 +26,13 @@ namespace MapControl CrsId = crsId; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { (var cosC, var x, var y) = GetPointValues(latitude, longitude); var h = cosC; // p.149 (20-5) - return new Point(h, h); + var scale = new Matrix(h, 0d, 0d, h, 0d, 0d); + return scale; } public override Point? LocationToMap(double latitude, double longitude) diff --git a/MapControl/Shared/PolarStereographicProjection.cs b/MapControl/Shared/PolarStereographicProjection.cs index d467aa07..dd19baab 100644 --- a/MapControl/Shared/PolarStereographicProjection.cs +++ b/MapControl/Shared/PolarStereographicProjection.cs @@ -1,6 +1,7 @@ using System; #if WPF using System.Windows; +using System.Windows.Media; #elif AVALONIA using Avalonia; #endif @@ -26,7 +27,7 @@ namespace MapControl public double FalseNorthing { get; set; } = 2e6; public Hemisphere Hemisphere { get; set; } - public static double RelativeScale(Hemisphere hemisphere, double flattening, double scaleFactor, double latitude) + public static double RelativeScale(Hemisphere hemisphere, double flattening, double latitude) { var sign = hemisphere == Hemisphere.North ? 1d : -1d; var phi = sign * latitude * Math.PI / 180d; @@ -37,20 +38,18 @@ namespace MapControl var t = Math.Tan(Math.PI / 4d - phi / 2d) / Math.Pow((1d - eSinPhi) / (1d + eSinPhi), e / 2d); // p.161 (15-9) - // r == ρ/a - var r = 2d * scaleFactor * t - / Math.Sqrt(Math.Pow(1d + e, 1d + e) * Math.Pow(1d - e, 1d - e)); // p.161 (21-33) - + // r == ρ/(a*k0), omit k0 for relative scale + var r = 2d * t / Math.Sqrt(Math.Pow(1d + e, 1d + e) * Math.Pow(1d - e, 1d - e)); // p.161 (21-33) var m = Math.Cos(phi) / Math.Sqrt(1d - eSinPhi * eSinPhi); // p.160 (14-15) return r / m; // p.161 (21-32) } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { - var k = RelativeScale(Hemisphere, Flattening, ScaleFactor, latitude); + var k = RelativeScale(Hemisphere, Flattening, latitude); - return new Point(k, k); + return new Matrix(k, 0d, 0d, k, 0d, 0d); } public override Point? LocationToMap(double latitude, double longitude) diff --git a/MapControl/Shared/StereographicProjection.cs b/MapControl/Shared/StereographicProjection.cs index 473e20f6..a1304be7 100644 --- a/MapControl/Shared/StereographicProjection.cs +++ b/MapControl/Shared/StereographicProjection.cs @@ -1,6 +1,7 @@ using System; #if WPF using System.Windows; +using System.Windows.Media; #elif AVALONIA using Avalonia; #endif @@ -25,12 +26,12 @@ namespace MapControl CrsId = crsId; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { (var cosC, var _, var _) = GetPointValues(latitude, longitude); var k = 2d / (1d + cosC); // p.157 (21-4), k0 == 1 - return new Point(k, k); + return new Matrix(k, 0d, 0d, k, 0d, 0d); } public override Point? LocationToMap(double latitude, double longitude) diff --git a/MapControl/Shared/TransverseMercatorProjection.cs b/MapControl/Shared/TransverseMercatorProjection.cs index 2980de5e..5413474d 100644 --- a/MapControl/Shared/TransverseMercatorProjection.cs +++ b/MapControl/Shared/TransverseMercatorProjection.cs @@ -61,11 +61,6 @@ namespace MapControl Flattening = Wgs84Flattening; } - public override Point RelativeScale(double latitude, double longitude) - { - return new Point(ScaleFactor, ScaleFactor); // sufficiently precise - } - public override Point? LocationToMap(double latitude, double longitude) { #if NETFRAMEWORK diff --git a/MapControl/Shared/WebMercatorProjection.cs b/MapControl/Shared/WebMercatorProjection.cs index 29616819..7faa3dd5 100644 --- a/MapControl/Shared/WebMercatorProjection.cs +++ b/MapControl/Shared/WebMercatorProjection.cs @@ -1,6 +1,7 @@ using System; #if WPF using System.Windows; +using System.Windows.Media; #elif AVALONIA using Avalonia; #endif @@ -26,11 +27,11 @@ namespace MapControl CrsId = crsId; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { var k = 1d / Math.Cos(latitude * Math.PI / 180d); // p.44 (7-3) - return new Point(k, k); + return new Matrix(k, 0d, 0d, k, 0d, 0d); } public override Point? LocationToMap(double latitude, double longitude) diff --git a/MapControl/Shared/WorldMercatorProjection.cs b/MapControl/Shared/WorldMercatorProjection.cs index b0d5cf31..b4bfee85 100644 --- a/MapControl/Shared/WorldMercatorProjection.cs +++ b/MapControl/Shared/WorldMercatorProjection.cs @@ -1,6 +1,7 @@ using System; #if WPF using System.Windows; +using System.Windows.Media; #elif AVALONIA using Avalonia; #endif @@ -28,11 +29,11 @@ namespace MapControl CrsId = crsId; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { var k = RelativeScale(latitude); - return new Point(k, k); + return new Matrix(k, 0d, 0d, k, 0d, 0d); } public override Point? LocationToMap(double latitude, double longitude) diff --git a/MapProjections/Shared/ProjNetMapProjection.cs b/MapProjections/Shared/ProjNetMapProjection.cs index 0246a7f3..39ecab94 100644 --- a/MapProjections/Shared/ProjNetMapProjection.cs +++ b/MapProjections/Shared/ProjNetMapProjection.cs @@ -93,13 +93,6 @@ namespace MapControl.Projections public MathTransform MapToLocationTransform { get; private set; } - public override Point RelativeScale(double latitude, double longitude) - { - var k = CoordinateSystem?.Projection?.GetParameter("scale_factor")?.Value ?? 1d; - - return new Point(k, k); - } - public override Point? LocationToMap(double latitude, double longitude) { if (LocationToMapTransform == null) diff --git a/MapProjections/Shared/WebMercatorProjection.cs b/MapProjections/Shared/WebMercatorProjection.cs index 923c53d4..36aa9a68 100644 --- a/MapProjections/Shared/WebMercatorProjection.cs +++ b/MapProjections/Shared/WebMercatorProjection.cs @@ -1,9 +1,7 @@ using ProjNet.CoordinateSystems; using System; #if WPF -using System.Windows; -#elif AVALONIA -using Avalonia; +using System.Windows.Media; #endif namespace MapControl.Projections @@ -19,11 +17,11 @@ namespace MapControl.Projections CoordinateSystem = ProjectedCoordinateSystem.WebMercator; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { var k = 1d / Math.Cos(latitude * Math.PI / 180d); // p.44 (7-3) - return new Point(k, k); + return new Matrix(k, 0d, 0d, k, 0d, 0d); } } } diff --git a/MapProjections/Shared/Wgs84UpsProjections.cs b/MapProjections/Shared/Wgs84UpsProjections.cs index a1628e6d..36d14f0b 100644 --- a/MapProjections/Shared/Wgs84UpsProjections.cs +++ b/MapProjections/Shared/Wgs84UpsProjections.cs @@ -1,7 +1,5 @@ #if WPF -using System.Windows; -#elif AVALONIA -using Avalonia; +using System.Windows.Media; #endif namespace MapControl.Projections @@ -23,11 +21,11 @@ namespace MapControl.Projections "AUTHORITY[\"EPSG\",\"32661\"]]"; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { - var k = PolarStereographicProjection.RelativeScale(Hemisphere.North, Wgs84Flattening, 0.994, latitude); + var k = PolarStereographicProjection.RelativeScale(Hemisphere.North, Wgs84Flattening, latitude); - return new Point(k, k); + return new Matrix(k, 0d, 0d, k, 0d, 0d); } } @@ -48,11 +46,11 @@ namespace MapControl.Projections "AUTHORITY[\"EPSG\",\"32761\"]]"; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { - var k = PolarStereographicProjection.RelativeScale(Hemisphere.South, Wgs84Flattening, 0.994, latitude); + var k = PolarStereographicProjection.RelativeScale(Hemisphere.South, Wgs84Flattening, latitude); - return new Point(k, k); + return new Matrix(k, 0d, 0d, k, 0d, 0d); } } } diff --git a/MapProjections/Shared/WorldMercatorProjection.cs b/MapProjections/Shared/WorldMercatorProjection.cs index 92ebb6f0..8a6b6352 100644 --- a/MapProjections/Shared/WorldMercatorProjection.cs +++ b/MapProjections/Shared/WorldMercatorProjection.cs @@ -1,7 +1,5 @@ #if WPF -using System.Windows; -#elif AVALONIA -using Avalonia; +using System.Windows.Media; #endif namespace MapControl.Projections @@ -29,11 +27,11 @@ namespace MapControl.Projections "AUTHORITY[\"EPSG\",\"3395\"]]"; } - public override Point RelativeScale(double latitude, double longitude) + public override Matrix RelativeScale(double latitude, double longitude) { var k = MapControl.WorldMercatorProjection.RelativeScale(latitude); - return new Point(k, k); + return new Matrix(k, 0d, 0d, k, 0d, 0d); } } }