Updated MapProjection and MapGraticule

This commit is contained in:
ClemensFischer 2026-02-01 01:42:24 +01:00
parent 2a45c1165c
commit cfed3575ac
16 changed files with 392 additions and 355 deletions

View file

@ -4,7 +4,6 @@ using Avalonia.Controls.Documents;
using Avalonia.Media;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace MapControl
{
@ -96,17 +95,9 @@ namespace MapControl
}
}
private static PathFigure CreatePolylineFigure(IEnumerable<Point> points)
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
{
var figure = new PathFigure
{
StartPoint = points.First(),
IsClosed = false,
IsFilled = false
};
figure.Segments.Add(new PolyLineSegment(points.Skip(1)));
return figure;
return new PolyLineSegment(points);
}
}
}

View file

@ -184,13 +184,13 @@ namespace MapControl
throw new ArgumentException($"Insufficient number of parameters in world file {worldFilePath}.");
}
return new Matrix(
parameters[0], // line 1: A or M11
parameters[1], // line 2: D or M12
parameters[2], // line 3: B or M21
parameters[3], // line 4: E or M22
parameters[4], // line 5: C or OffsetX
parameters[5]); // line 6: F or OffsetY
return new Matrix( // https://en.wikipedia.org/wiki/World_file
parameters[0], // line 1: A
parameters[1], // line 2: D
parameters[2], // line 3: B
parameters[3], // line 4: E
parameters[4], // line 5: C
parameters[5]); // line 6: F
}
private static MapProjection GetProjection(short[] geoKeyDirectory)

View file

@ -375,8 +375,7 @@ namespace MapControl
if (projection.IsNormalCylindrical)
{
var maxLocation = projection.MapToLocation(0d, 180d * MapProjection.Wgs84MeterPerDegree);
maxLatitude = maxLocation.Latitude;
maxLatitude = projection.MapToLocation(0d, 180d * MapProjection.Wgs84MeterPerDegree).Latitude;
Center = CoerceCenterProperty(Center);
}

View file

@ -40,6 +40,12 @@ namespace MapControl
public static readonly DependencyProperty StrokeThicknessProperty =
DependencyPropertyHelper.Register<MapGraticule, double>(nameof(StrokeThickness), 0.5);
private static readonly double[] lineDistances = [
1d/3600d, 1d/1800d, 1d/720d, 1d/360d, 1d/240d, 1d/120d,
1d/60d, 1d/30d, 1d/12d, 1d/6d, 1d/4d, 1d/2d,
1d, 2d, 5d, 10d, 15d, 30d];
/// <summary>
/// Minimum graticule line distance in pixels. The default value is 150.
/// </summary>
@ -75,32 +81,31 @@ namespace MapControl
private List<Label> DrawGraticule(PathFigureCollection figures)
{
figures.Clear();
var labels = new List<Label>();
var lineDistance = GetLineDistance();
var labelFormat = lineDistance < 1d / 60d ? "{0} {1}°{2:00}'{3:00}\""
: lineDistance < 1d ? "{0} {1}°{2:00}'" : "{0} {1}°";
figures.Clear();
if (ParentMap.MapProjection.IsNormalCylindrical)
{
DrawCylindricalGraticule(figures, labels, lineDistance, labelFormat);
DrawNormalCylindrical(figures, labels);
}
else
{
DrawGraticule(figures, labels, lineDistance, labelFormat);
DrawGraticule(figures, labels);
}
return labels;
}
private void DrawCylindricalGraticule(PathFigureCollection figures, List<Label> labels, double lineDistance, string labelFormat)
private void DrawNormalCylindrical(PathFigureCollection figures, List<Label> labels)
{
var southWest = ParentMap.ViewToLocation(new Point(0d, ParentMap.ActualHeight));
var northEast = ParentMap.ViewToLocation(new Point(ParentMap.ActualWidth, 0d));
var lineDistance = GetLineDistance(MinLineDistance /
(ParentMap.ViewTransform.Scale * MapProjection.Wgs84MeterPerDegree));
var latLabelStart = Math.Ceiling(southWest.Latitude / lineDistance) * lineDistance;
var lonLabelStart = Math.Ceiling(southWest.Longitude / lineDistance) * lineDistance;
var labelFormat = GetLabelFormat(Math.Min(lineDistance, lineDistance));
for (var lat = latLabelStart; lat <= northEast.Latitude; lat += lineDistance)
{
@ -122,43 +127,52 @@ namespace MapControl
}
}
private void DrawGraticule(PathFigureCollection figures, List<Label> labels, double lineDistance, string labelFormat)
private void DrawGraticule(PathFigureCollection figures, List<Label> labels)
{
var lineDistance = GetLineDistance(MinLineDistance /
(ParentMap.ViewTransform.Scale * MapProjection.Wgs84MeterPerDegree * Math.Cos(ParentMap.Center.Latitude * Math.PI / 180d)));
var labelFormat = GetLabelFormat(Math.Min(lineDistance, lineDistance));
GetLatitudeRange(lineDistance, out double minLat, out double maxLat);
var latSegments = (int)Math.Round(Math.Abs(maxLat - minLat) / lineDistance);
var latSegments = (int)Math.Ceiling(Math.Abs(maxLat - minLat) / lineDistance);
var interpolationCount = Math.Max(1, (int)Math.Ceiling(lineDistance));
var interpolationDistance = lineDistance / interpolationCount;
var latPoints = latSegments * interpolationCount;
var interpolationStep = lineDistance / interpolationCount;
var latStep = lineDistance;
var startLat = minLat;
if (Math.Abs(minLat) > Math.Abs(maxLat))
{
startLat = maxLat;
latStep = -lineDistance;
}
var centerLon = Math.Round(ParentMap.Center.Longitude / lineDistance) * lineDistance;
var minLon = centerLon - lineDistance;
var minLon = centerLon;
var maxLon = centerLon + lineDistance;
if (DrawMeridian(figures, centerLon, minLat, interpolationDistance, latPoints))
while (DrawMeridian(figures, minLon, minLat, interpolationStep, latSegments * interpolationCount) &&
minLon > centerLon - 180d)
{
while (DrawMeridian(figures, minLon, minLat, interpolationDistance, latPoints) &&
minLon > centerLon - 180d)
{
minLon -= lineDistance;
}
minLon -= lineDistance;
}
while (DrawMeridian(figures, maxLon, minLat, interpolationDistance, latPoints) &&
maxLon < centerLon + 180d)
{
maxLon += lineDistance;
}
while (DrawMeridian(figures, maxLon, minLat, interpolationStep, latSegments * interpolationCount) &&
maxLon < centerLon + 180d)
{
maxLon += lineDistance;
}
var lonSegments = (int)Math.Round(Math.Abs(maxLon - minLon) / lineDistance);
for (var i = 1; i < latSegments; i++)
{
var lat = minLat + i * lineDistance;
var lat = startLat + i * latStep;
var lon = minLon;
var points = new List<Point>();
var mapPoint = ParentMap.MapProjection.LocationToMap(lat, lon);
var position = ParentMap.ViewTransform.MapToView(mapPoint);
var rotation = -ParentMap.MapProjection.GridConvergence(mapPoint.X, mapPoint.Y);
var position = ParentMap.LocationToView(lat, lon);
var rotation = -ParentMap.MapProjection.GridConvergence(lat, lon);
points.Add(position);
AddLabel(labels, labelFormat, lat, lon, position, rotation);
@ -167,13 +181,12 @@ namespace MapControl
{
for (int k = 1; k <= interpolationCount; k++)
{
lon = minLon + j * lineDistance + k * interpolationDistance;
mapPoint = ParentMap.MapProjection.LocationToMap(lat, lon);
position = ParentMap.ViewTransform.MapToView(mapPoint);
lon = minLon + j * lineDistance + k * interpolationStep;
position = ParentMap.LocationToView(lat, lon);
points.Add(position);
}
rotation = -ParentMap.MapProjection.GridConvergence(mapPoint.X, mapPoint.Y);
rotation = -ParentMap.MapProjection.GridConvergence(lat, lon);
AddLabel(labels, labelFormat, lat, lon, position, rotation);
}
@ -222,18 +235,16 @@ namespace MapControl
if (minLat > -90d || maxLat < 90d)
{
var width = ParentMap.ActualWidth;
var height = ParentMap.ActualHeight;
var locations = new Location[]
{
ParentMap.ViewToLocation(new Point(0d, 0d)),
ParentMap.ViewToLocation(new Point(width, 0d)),
ParentMap.ViewToLocation(new Point(0d, height)),
ParentMap.ViewToLocation(new Point(width, height)),
ParentMap.ViewToLocation(new Point(width / 2d, 0d)),
ParentMap.ViewToLocation(new Point(width / 2d, height)),
ParentMap.ViewToLocation(new Point(0d, height / 2d)),
ParentMap.ViewToLocation(new Point(width, height / 2d)),
ParentMap.ViewToLocation(new Point(ParentMap.ActualWidth, 0d)),
ParentMap.ViewToLocation(new Point(0d, ParentMap.ActualHeight)),
ParentMap.ViewToLocation(new Point(ParentMap.ActualWidth, ParentMap.ActualHeight)),
ParentMap.ViewToLocation(new Point(ParentMap.ActualWidth / 2d, 0d)),
ParentMap.ViewToLocation(new Point(ParentMap.ActualWidth / 2d, ParentMap.ActualHeight)),
ParentMap.ViewToLocation(new Point(0d, ParentMap.ActualHeight / 2d)),
ParentMap.ViewToLocation(new Point(ParentMap.ActualWidth, ParentMap.ActualHeight / 2d)),
};
var latitudes = locations.Select(loc => loc.Latitude).Distinct();
@ -241,32 +252,8 @@ namespace MapControl
maxLat = Math.Max(maxLat, latitudes.Max());
}
minLatitude = Math.Max(Math.Floor(minLat / lineDistance - 1d) * lineDistance, -90d);
maxLatitude = Math.Min(Math.Ceiling(maxLat / lineDistance + 1d) * lineDistance, 90d);
}
private double GetLineDistance()
{
var pixelPerDegree = ParentMap.ViewTransform.Scale * MapProjection.Wgs84MeterPerDegree;
if (!ParentMap.MapProjection.IsNormalCylindrical)
{
pixelPerDegree *= Math.Cos(ParentMap.Center.Latitude * Math.PI / 180d);
}
var minDistance = MinLineDistance / Math.Max(1d, pixelPerDegree);
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;
while (i < lineDistances.Length - 1 && lineDistances[i] < minDistance)
{
i++;
}
return Math.Min(lineDistances[i] / scale, 30d);
minLatitude = Math.Max(Math.Floor(minLat / lineDistance) * lineDistance, -90d);
maxLatitude = Math.Min(Math.Ceiling(maxLat / lineDistance) * lineDistance, 90d);
}
private void AddLabel(List<Label> labels, string labelFormat, double latitude, double longitude, Point position, double rotation = 0d)
@ -291,6 +278,20 @@ namespace MapControl
}
}
private static double GetLineDistance(double minDistance)
{
minDistance = Math.Max(minDistance, lineDistances.First());
minDistance = Math.Min(minDistance, lineDistances.Last());
return lineDistances.First(d => d >= minDistance);
}
private static string GetLabelFormat(double lineDistance)
{
return lineDistance < 1d / 60d ? "{0} {1}°{2:00}'{3:00}\"" :
lineDistance < 1d ? "{0} {1}°{2:00}'" : "{0} {1}°";
}
private static string GetLabelText(double value, string labelFormat, string hemispheres)
{
var hemisphere = hemispheres[0];
@ -318,5 +319,18 @@ namespace MapControl
figure.Segments.Add(new LineSegment { Point = p2 });
return figure;
}
private static PathFigure CreatePolylineFigure(IEnumerable<Point> points)
{
var figure = new PathFigure
{
StartPoint = points.First(),
IsClosed = false,
IsFilled = false
};
figure.Segments.Add(CreatePolyLineSegment(points.Skip(1)));
return figure;
}
}
}

View file

@ -76,7 +76,7 @@ namespace MapControl
{
var longitudeOffset = 0d;
if (ParentMap.MapProjection.IsNormalCylindrical && location != null)
if (location != null != ParentMap.MapProjection.IsNormalCylindrical)
{
var position = ParentMap.LocationToView(location);

View file

@ -11,6 +11,7 @@ namespace MapControl
/// <summary>
/// Implements a map projection, a transformation between geographic coordinates,
/// i.e. latitude and longitude in degrees, and cartesian map coordinates in meters.
/// See https://en.wikipedia.org/wiki/Map_projection.
/// </summary>
#if UWP || WINUI
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
@ -40,7 +41,7 @@ namespace MapControl
/// <summary>
/// Gets the WMS 1.3.0 CRS identifier.
/// </summary>
public string CrsId { get; protected set; } = "";
public string CrsId { get; protected set; }
/// <summary>
/// Indicates whether the projection is normal cylindrical, see
@ -48,10 +49,16 @@ namespace MapControl
/// </summary>
public bool IsNormalCylindrical { get; protected set; }
/// <summary>
/// The earth ellipsoid semi-major axis, or spherical earth radius respectively, in meters.
/// </summary>
public double EquatorialRadius { get; set; } = Wgs84EquatorialRadius;
public double CentralMeridian { get; set; }
public double LatitudeOfOrigin { get; set; }
/// <summary>
/// Gets the grid convergence angle in degrees at the specified geographic coordinates.
/// Used for rotating the Rect resulting from BoundingBoxToMap in non-normal-cylindrical
/// projections, i.e. Transverse Mercator and Polar Stereographic.
/// </summary>
public virtual double GridConvergence(double latitude, double longitude) => 0d;
/// <summary>
/// Gets the relative transform at the specified geographic coordinates.
@ -69,13 +76,6 @@ namespace MapControl
/// </summary>
public abstract Location MapToLocation(double x, double y);
/// <summary>
/// Gets the grid convergence angle in degrees at the specified projected map coordinates.
/// Used for rotating the Rect resulting from BoundingBoxToMap in non-normal-cylindrical
/// projections, i.e. Transverse Mercator and Polar Stereographic.
/// </summary>
public virtual double GridConvergence(double x, double y) => 0d;
/// <summary>
/// Gets the relative transform at the specified geographic Location.
/// </summary>
@ -114,17 +114,22 @@ namespace MapControl
var north = new Point((northWest.X + northEast.X) / 2d, (northWest.Y + northEast.Y) / 2d);
var centerX = (south.X + north.X) / 2d;
var centerY = (south.Y + north.Y) / 2d;
var dx = north.X - south.X;
var dy = north.Y - south.Y;
var dxW = northWest.X - southWest.X;
var dyW = northWest.Y - southWest.Y;
var dxE = northEast.X - southEast.X;
var dyE = northEast.Y - southEast.Y;
var dxS = southEast.X - southWest.X;
var dyS = southEast.Y - southWest.Y;
var dxN = northEast.X - northWest.X;
var dyN = northEast.Y - northWest.Y;
var width = (Math.Sqrt(dxS * dxS + dyS * dyS) + Math.Sqrt(dxN * dxN + dyN * dyN)) / 2d;
var height = Math.Sqrt(dx * dx + dy * dy);
var height = (Math.Sqrt(dxW * dxW + dyW * dyW) + Math.Sqrt(dxE * dxE + dyE * dyE)) / 2d;
rect = new Rect(centerX - width / 2d, centerY - height / 2d, width, height);
rotation = -GridConvergence(centerX, centerY); // invert direction for RotateTransform
rotation = -GridConvergence( // invert direction for RotateTransform
(boundingBox.South + boundingBox.North) / 2d,
(boundingBox.West + boundingBox.East) / 2d);
}
return (rect, rotation);

View file

@ -1,25 +1,54 @@
using System;
using System.Globalization;
namespace MapControl
{
public class MapProjectionFactory
{
public virtual MapProjection GetProjection(string crsId)
public MapProjection GetProjection(string crsId)
{
var projection = crsId switch
var projection = CreateProjection(crsId);
if (projection == null &&
crsId.StartsWith("EPSG:") &&
int.TryParse(crsId.Substring(5), out int epsgCode))
{
WebMercatorProjection.DefaultCrsId => new WebMercatorProjection(),
WorldMercatorProjection.DefaultCrsId => new WorldMercatorProjection(),
EquirectangularProjection.DefaultCrsId or "CRS:84" => new EquirectangularProjection(crsId),
Wgs84UpsNorthProjection.DefaultCrsId => new Wgs84UpsNorthProjection(),
Wgs84UpsSouthProjection.DefaultCrsId => new Wgs84UpsSouthProjection(),
_ => GetProjectionFromEpsgCode(crsId),
};
projection = CreateProjection(epsgCode);
}
return projection ?? throw new NotSupportedException($"MapProjection \"{crsId}\" is not supported.");
}
public virtual MapProjection GetProjection(int epsgCode)
protected virtual MapProjection CreateProjection(string crsId)
{
MapProjection projection = crsId switch
{
WebMercatorProjection.DefaultCrsId => new WebMercatorProjection(),
WorldMercatorProjection.DefaultCrsId => new WorldMercatorProjection(),
Wgs84UpsNorthProjection.DefaultCrsId => new Wgs84UpsNorthProjection(),
Wgs84UpsSouthProjection.DefaultCrsId => new Wgs84UpsSouthProjection(),
EquirectangularProjection.DefaultCrsId or "CRS:84" => new EquirectangularProjection(crsId),
_ => null,
};
if (projection == null && crsId.StartsWith(StereographicProjection.DefaultCrsId))
{
var parameters = crsId.Split(',');
if (parameters.Length == 4 &&
parameters[0] == StereographicProjection.DefaultCrsId &&
parameters[1] == "1" &&
double.TryParse(parameters[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double longitude) &&
double.TryParse(parameters[3], NumberStyles.Float, CultureInfo.InvariantCulture, out double latitude))
{
projection = new StereographicProjection(longitude, latitude);
}
}
return projection;
}
protected virtual MapProjection CreateProjection(int epsgCode)
{
return epsgCode switch
{
@ -36,10 +65,5 @@ namespace MapControl
_ => null
};
}
protected MapProjection GetProjectionFromEpsgCode(string crsId)
{
return crsId.StartsWith("EPSG:") && int.TryParse(crsId.Substring(5), out int epsgCode) ? GetProjection(epsgCode) : null;
}
}
}

View file

@ -19,11 +19,37 @@ namespace MapControl
public double ScaleFactor { get; set; } = 0.994;
public double FalseEasting { get; set; } = 2e6;
public double FalseNorthing { get; set; } = 2e6;
public Hemisphere Hemisphere { get; set; }
public override double GridConvergence(double latitude, double longitude)
{
return Math.Sign(LatitudeOfOrigin) * (longitude - CentralMeridian);
}
public override Matrix RelativeTransform(double latitude, double longitude)
{
var sign = Math.Sign(LatitudeOfOrigin);
var phi = sign * latitude * Math.PI / 180d;
var e = Math.Sqrt((2d - Flattening) * Flattening);
var eSinPhi = e * Math.Sin(phi);
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)
var m = Math.Cos(phi) / Math.Sqrt(1d - eSinPhi * eSinPhi); // p.160 (14-15)
var k = r / m; // p.161 (21-32)
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
transform.Rotate(-sign * longitude);
return transform;
}
public override Point LocationToMap(double latitude, double longitude)
{
var sign = Hemisphere == Hemisphere.North ? 1d : -1d;
var sign = Math.Sign(LatitudeOfOrigin);
var phi = sign * latitude * Math.PI / 180d;
var lambda = sign * longitude * Math.PI / 180d;
@ -42,30 +68,9 @@ namespace MapControl
return new Point(x + FalseEasting, y + FalseNorthing);
}
public override Matrix RelativeTransform(double latitude, double longitude)
{
var sign = Hemisphere == Hemisphere.North ? 1d : -1d;
var phi = sign * latitude * Math.PI / 180d;
var e = Math.Sqrt((2d - Flattening) * Flattening);
var eSinPhi = e * Math.Sin(phi);
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)
var m = Math.Cos(phi) / Math.Sqrt(1d - eSinPhi * eSinPhi); // p.160 (14-15)
var k = r / m; // p.161 (21-32)
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
transform.Rotate(-sign * longitude);
return transform;
}
public override Location MapToLocation(double x, double y)
{
var sign = Hemisphere == Hemisphere.North ? 1d : -1d;
var sign = Math.Sign(LatitudeOfOrigin);
x = sign * (x - FalseEasting);
y = sign * (y - FalseNorthing);
@ -80,17 +85,6 @@ namespace MapControl
return new Location(sign * phi * 180d / Math.PI, sign * lambda * 180d / Math.PI);
}
public override double GridConvergence(double x, double y)
{
var sign = Hemisphere == Hemisphere.North ? 1d : -1d;
x = sign * (x - FalseEasting);
y = sign * (y - FalseNorthing);
var lambda = Math.Atan2(x, -y); // p.162 (20-16)
return lambda * 180d / Math.PI;
}
}
/// <summary>
@ -108,7 +102,7 @@ namespace MapControl
public Wgs84UpsNorthProjection(string crsId)
{
CrsId = crsId;
Hemisphere = Hemisphere.North;
LatitudeOfOrigin = 90d;
}
}
@ -127,7 +121,7 @@ namespace MapControl
public Wgs84UpsSouthProjection(string crsId)
{
CrsId = crsId;
Hemisphere = Hemisphere.South;
LatitudeOfOrigin = -90d;
}
}
}

View file

@ -0,0 +1,97 @@
using System;
using System.Globalization;
#if WPF
using System.Windows;
using System.Windows.Media;
#elif AVALONIA
using Avalonia;
#endif
namespace MapControl
{
/// <summary>
/// Spherical Stereographic Projection - AUTO2:97002.
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.157-160.
/// </summary>
public class StereographicProjection : MapProjection
{
public const string DefaultCrsId = "AUTO2:97002"; // GeoServer non-standard CRS identifier
public StereographicProjection(double centerLongitude, double centerLatitude, string crsId = DefaultCrsId)
{
CentralMeridian = centerLongitude;
LatitudeOfOrigin = centerLatitude;
CrsId = string.Format(CultureInfo.InvariantCulture,
"{0},1,{1:0.########},{2:0.########}", crsId, centerLongitude, centerLatitude);
}
public override double GridConvergence(double latitude, double longitude)
{
var p0 = LocationToMap(latitude, longitude);
var p1 = LocationToMap(latitude + 1e-3, longitude);
return Math.Atan2(p0.X - p1.X, p1.Y - p0.Y) * 180d / Math.PI;
}
public override Matrix RelativeTransform(double latitude, double longitude)
{
return new Matrix(1d, 0d, 0d, 1d, 0d, 0d);
}
public override Point LocationToMap(double latitude, double longitude)
{
var phi = latitude * Math.PI / 180d;
var phi1 = LatitudeOfOrigin * Math.PI / 180d;
var dLambda = (longitude - CentralMeridian) * Math.PI / 180d; // λ - λ0
var cosPhi = Math.Cos(phi);
var sinPhi = Math.Sin(phi);
var cosPhi1 = Math.Cos(phi1);
var sinPhi1 = Math.Sin(phi1);
var cosLambda = Math.Cos(dLambda);
var sinLambda = Math.Sin(dLambda);
var x = cosPhi * sinLambda;
var y = cosPhi1 * sinPhi - sinPhi1 * cosPhi * cosLambda;
var cosC = sinPhi1 * sinPhi + cosPhi1 * cosPhi * cosLambda; // (5-3)
cosC = Math.Min(Math.Max(cosC, -1d), 1d); // protect against rounding errors
var k = 2d / (1d + cosC); // p.157 (21-4), k0 == 1
return new Point(
EquatorialRadius * k * x,
EquatorialRadius * k * y); // p.157 (21-2/3)
}
public override Location MapToLocation(double x, double y)
{
var rho = Math.Sqrt(x * x + y * y);
var c = 2d * Math.Atan(rho / (2d * EquatorialRadius)); // p.159 (21-15), k0 == 1
var cosC = Math.Cos(c);
var sinC = Math.Sin(c);
var phi1 = LatitudeOfOrigin * Math.PI / 180d;
var cosPhi1 = Math.Cos(phi1);
var sinPhi1 = Math.Sin(phi1);
var phi = Math.Asin(cosC * sinPhi1 + y * sinC * cosPhi1 / rho); // (20-14)
double u, v;
if (LatitudeOfOrigin == 90d) // (20-16)
{
u = x;
v = -y;
}
else if (LatitudeOfOrigin == -90d) // (20-17)
{
u = x;
v = y;
}
else // (20-15)
{
u = x * sinC;
v = rho * cosPhi1 * cosC - y * sinPhi1 * sinC;
}
return new Location(
phi * 180d / Math.PI,
Math.Atan2(u, v) * 180d / Math.PI + CentralMeridian);
}
}
}

View file

@ -9,9 +9,10 @@ using Avalonia;
namespace MapControl
{
/// <summary>
/// Transverse Mercator Projection.
/// See https://en.wikipedia.org/wiki/Transverse_Mercator_projection
/// and https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system.
/// Transverse Mercator Projection. See
/// https://en.wikipedia.org/wiki/Transverse_Mercator_projection,
/// https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system,
/// https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Convergence.
/// </summary>
public class TransverseMercatorProjection : MapProjection
{
@ -27,6 +28,11 @@ namespace MapControl
private double f1; // A/a
private double f2; // 2*sqrt(n)/(1+n)
public TransverseMercatorProjection()
{
Flattening = Wgs84Flattening;
}
public double Flattening
{
get;
@ -50,43 +56,20 @@ namespace MapControl
}
}
public double CentralMeridian { get; set; }
public double ScaleFactor { get; set; } = 0.9996;
public double FalseEasting { get; set; } = 5e5;
public double FalseNorthing { get; set; }
public TransverseMercatorProjection()
{
Flattening = Wgs84Flattening;
}
public override Point LocationToMap(double latitude, double longitude)
public override double GridConvergence(double latitude, double longitude)
{
// φ
var phi = latitude * Math.PI / 180d;
var sinPhi = Math.Sin(phi);
// t
var t = Math.Sinh(Atanh(sinPhi) - f2 * Atanh(f2 * sinPhi));
// λ - λ0
var dLambda = (longitude - CentralMeridian) * Math.PI / 180d;
// ξ'
var xi_ = Math.Atan2(t, Math.Cos(dLambda));
// η'
var eta_ = Atanh(Math.Sin(dLambda) / Math.Sqrt(1d + t * t));
// k0 * A
var k0A = ScaleFactor * EquatorialRadius * f1;
var x = FalseEasting + k0A * (eta_ +
a1 * Math.Cos(2d * xi_) * Math.Sinh(2d * eta_) +
a2 * Math.Cos(4d * xi_) * Math.Sinh(4d * eta_) +
a3 * Math.Cos(6d * xi_) * Math.Sinh(6d * eta_));
var y = FalseNorthing + k0A * (xi_ +
a1 * Math.Sin(2d * xi_) * Math.Cosh(2d * eta_) +
a2 * Math.Sin(4d * xi_) * Math.Cosh(4d * eta_) +
a3 * Math.Sin(6d * xi_) * Math.Cosh(6d * eta_));
return new Point(x, y);
// γ calculation for the sphere is sufficiently accurate
//
return Math.Atan(Math.Tan(dLambda) * Math.Sin(phi)) * 180d / Math.PI;
}
public override Matrix RelativeTransform(double latitude, double longitude)
@ -120,7 +103,7 @@ namespace MapControl
var m = (1d - n) / (1d + n) * Math.Tan(phi);
var k = ScaleFactor * f1 * Math.Sqrt((1d + m * m) * (sigma * sigma + tau * tau) / (t * t + cosLambda * cosLambda));
// γ, meridian convergence angle
// γ, grid convergence
var gamma = Math.Atan2(tau * u + sigma * t * tanLambda, sigma * u - tau * t * tanLambda);
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
@ -129,6 +112,35 @@ namespace MapControl
return transform;
}
public override Point LocationToMap(double latitude, double longitude)
{
// φ
var phi = latitude * Math.PI / 180d;
var sinPhi = Math.Sin(phi);
// t
var t = Math.Sinh(Atanh(sinPhi) - f2 * Atanh(f2 * sinPhi));
// λ - λ0
var dLambda = (longitude - CentralMeridian) * Math.PI / 180d;
// ξ'
var xi_ = Math.Atan2(t, Math.Cos(dLambda));
// η'
var eta_ = Atanh(Math.Sin(dLambda) / Math.Sqrt(1d + t * t));
// k0 * A
var k0A = ScaleFactor * EquatorialRadius * f1;
var x = FalseEasting + k0A * (eta_ +
a1 * Math.Cos(2d * xi_) * Math.Sinh(2d * eta_) +
a2 * Math.Cos(4d * xi_) * Math.Sinh(4d * eta_) +
a3 * Math.Cos(6d * xi_) * Math.Sinh(6d * eta_));
var y = FalseNorthing + k0A * (xi_ +
a1 * Math.Sin(2d * xi_) * Math.Cosh(2d * eta_) +
a2 * Math.Sin(4d * xi_) * Math.Cosh(4d * eta_) +
a3 * Math.Sin(6d * xi_) * Math.Cosh(6d * eta_));
return new Point(x, y);
}
public override Location MapToLocation(double x, double y)
{
// k0 * A
@ -162,42 +174,6 @@ namespace MapControl
dLambda * 180d / Math.PI + CentralMeridian);
}
public override double GridConvergence(double x, double y)
{
// k0 * A
var k0A = ScaleFactor * EquatorialRadius * f1;
// ξ
var xi = (y - FalseNorthing) / k0A;
// η
var eta = (x - FalseEasting) / k0A;
// ξ'
var xi_ = xi -
b1 * Math.Sin(2d * xi) * Math.Cosh(2d * eta) -
b2 * Math.Sin(4d * xi) * Math.Cosh(4d * eta) -
b3 * Math.Sin(6d * xi) * Math.Cosh(6d * eta);
// η'
var eta_ = eta -
b1 * Math.Cos(2d * xi) * Math.Sinh(2d * eta) -
b2 * Math.Cos(4d * xi) * Math.Sinh(4d * eta) -
b3 * Math.Cos(6d * xi) * Math.Sinh(6d * eta);
// σ'
var sigma_ = 1 -
2d * b1 * Math.Cos(2d * xi) * Math.Cosh(2d * eta) +
4d * b2 * Math.Cos(4d * xi) * Math.Cosh(4d * eta) +
6d * b3 * Math.Cos(6d * xi) * Math.Cosh(6d * eta);
// τ'
var tau_ =
2d * b1 * Math.Sin(2d * xi) * Math.Sinh(2d * eta) +
4d * b2 * Math.Sin(4d * xi) * Math.Sinh(4d * eta) +
6d * b3 * Math.Sin(6d * xi) * Math.Sinh(6d * eta);
var tanXi_tanhEta_ = Math.Tan(xi_) * Math.Tanh(eta_);
// γ
var gamma = Math.Atan2(tau_ + sigma_ * tanXi_tanhEta_, sigma_ + -tau_ * tanXi_tanhEta_);
return gamma * 180d / Math.PI;
}
#if NETFRAMEWORK
private static double Atanh(double x) => Math.Log((1d + x) / (1d - x)) / 2d;
#else

View file

@ -10,45 +10,68 @@ namespace MapControl
{
/// <summary>
/// Elliptical Transverse Mercator Projection.
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.60-64.
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/publication/pp1395), p.60-64,
/// and https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Convergence.
/// </summary>
public class TransverseMercatorProjectionSnyder : MapProjection
{
private double M0;
public double Flattening { get; set; } = Wgs84Flattening;
public double CentralMeridian { get; set; }
public double ScaleFactor { get; set; } = 0.9996;
public double FalseEasting { get; set; } = 5e5;
public double FalseNorthing { get; set; }
public double LatitudeOfOrigin
public override double GridConvergence(double latitude, double longitude)
{
get;
set
{
field = value;
M0 = MeridianDistance(value * Math.PI / 180d);
}
// φ
var phi = latitude * Math.PI / 180d;
// λ - λ0
var dLambda = (longitude - CentralMeridian) * Math.PI / 180d;
// γ calculation for the sphere is sufficiently accurate
//
return Math.Atan(Math.Tan(dLambda) * Math.Sin(phi)) * 180d / Math.PI;
}
private double MeridianDistance(double phi)
public override Matrix RelativeTransform(double latitude, double longitude)
{
var e2 = (2d - Flattening) * Flattening;
var e4 = e2 * e2;
var e6 = e2 * e4;
var k = ScaleFactor;
var gamma = 0d; // γ
return EquatorialRadius * (
(1d - e2 / 4d - e4 * 3d / 64d - e6 * 5d / 256d) * phi -
(e2 * 3d / 8d + e4 * 3d / 32d + e6 * 45d / 1024d) * Math.Sin(2d * phi) +
(e4 * 15d / 256d + e6 * 45d / 1024d) * Math.Sin(4d * phi) -
e6 * 35d / 3072d * Math.Sin(6d * phi)); // (3-21)
if (latitude > -90d && latitude < 90d)
{
var phi = latitude * Math.PI / 180d;
var sinPhi = Math.Sin(phi);
var cosPhi = Math.Cos(phi);
var tanPhi = sinPhi / cosPhi;
var dLambda = (longitude - CentralMeridian) * Math.PI / 180d;
var e2 = (2d - Flattening) * Flattening;
var e_2 = e2 / (1d - e2); // (8-12)
var T = tanPhi * tanPhi; // (8-13)
var C = e_2 * cosPhi * cosPhi; // (8-14)
var A = dLambda * cosPhi; // (8-15)
var A2 = A * A;
var A4 = A2 * A2;
var A6 = A2 * A4;
k *= 1d + (1d + C) * A2 / 2d +
(5d - 4d * T + 42d * C + 13d * C * C - 28d * e_2) * A4 / 24d +
(61d - 148d * T + 16 * T * T) * A6 / 720d; // (8-11)
gamma = Math.Atan(Math.Tan(dLambda) * sinPhi) * 180d / Math.PI;
}
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
transform.Rotate(-gamma);
return transform;
}
public override Point LocationToMap(double latitude, double longitude)
{
var phi = latitude * Math.PI / 180d;
var M = MeridianDistance(phi);
var M0 = MeridianDistance(LatitudeOfOrigin * Math.PI / 180d);
double x, y;
if (latitude > -90d && latitude < 90d)
@ -83,41 +106,6 @@ namespace MapControl
return new Point(x + FalseEasting, y + FalseNorthing);
}
public override Matrix RelativeTransform(double latitude, double longitude)
{
var k = ScaleFactor;
var gamma = 0d; // γ, https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Convergence
if (latitude > -90d && latitude < 90d)
{
var phi = latitude * Math.PI / 180d;
var sinPhi = Math.Sin(phi);
var cosPhi = Math.Cos(phi);
var tanPhi = sinPhi / cosPhi;
var dLambda = (longitude - CentralMeridian) * Math.PI / 180d;
var e2 = (2d - Flattening) * Flattening;
var e_2 = e2 / (1d - e2); // (8-12)
var T = tanPhi * tanPhi; // (8-13)
var C = e_2 * cosPhi * cosPhi; // (8-14)
var A = dLambda * cosPhi; // (8-15)
var A2 = A * A;
var A4 = A2 * A2;
var A6 = A2 * A4;
k *= 1d + (1d + C) * A2 / 2d +
(5d - 4d * T + 42d * C + 13d * C * C - 28d * e_2) * A4 / 24d +
(61d - 148d * T + 16 * T * T) * A6 / 720d; // (8-11)
gamma = Math.Atan(Math.Tan(dLambda) * sinPhi) * 180d / Math.PI;
}
var transform = new Matrix(k, 0d, 0d, k, 0d, 0d);
transform.Rotate(-gamma);
return transform;
}
public override Location MapToLocation(double x, double y)
{
var e2 = (2d - Flattening) * Flattening;
@ -129,6 +117,7 @@ namespace MapControl
var e13 = e1 * e12;
var e14 = e1 * e13;
var M0 = MeridianDistance(LatitudeOfOrigin * Math.PI / 180d);
var M = M0 + (y - FalseNorthing) / ScaleFactor; // (8-20)
var mu = M / (EquatorialRadius * (1d - e2 / 4d - e4 * 3d / 64d - e6 * 5d / 256d)); // (7-19)
var phi1 = mu +
@ -165,51 +154,17 @@ namespace MapControl
dLambda * 180d / Math.PI + CentralMeridian);
}
public override double GridConvergence(double x, double y)
private double MeridianDistance(double phi)
{
var e2 = (2d - Flattening) * Flattening;
var e4 = e2 * e2;
var e6 = e2 * e4;
var s = Math.Sqrt(1d - e2);
var e1 = (1d - s) / (1d + s); // (3-24)
var e12 = e1 * e1;
var e13 = e1 * e12;
var e14 = e1 * e13;
var M = M0 + (y - FalseNorthing) / ScaleFactor; // (8-20)
var mu = M / (EquatorialRadius * (1d - e2 / 4d - e4 * 3d / 64d - e6 * 5d / 256d)); // (7-19)
var phi1 = mu +
(e1 * 3d / 2d - e13 * 27d / 32d) * Math.Sin(2d * mu) +
(e12 * 21d / 16d - e14 * 55d / 32d) * Math.Sin(4d * mu) +
e13 * 151d / 96d * Math.Sin(6d * mu) +
e14 * 1097d / 512d * Math.Sin(8d * mu); // (3-26)
var sinPhi1 = Math.Sin(phi1);
var cosPhi1 = Math.Cos(phi1);
var tanPhi1 = sinPhi1 / cosPhi1;
var e_2 = e2 / (1d - e2); // (8-12)
var C1 = e_2 * cosPhi1 * cosPhi1; // (8-21)
var T1 = sinPhi1 * sinPhi1 / (cosPhi1 * cosPhi1); // (8-22)
s = Math.Sqrt(1d - e2 * sinPhi1 * sinPhi1);
var N1 = EquatorialRadius / s; // (8-23)
var R1 = EquatorialRadius * (1d - e2) / (s * s * s); // (8-24)
var D = (x - FalseEasting) / (N1 * ScaleFactor); // (8-25)
var D2 = D * D;
var D3 = D * D2;
var D4 = D * D3;
var D5 = D * D4;
var D6 = D * D5;
var phi = phi1 - N1 * tanPhi1 / R1 * (D2 / 2d - (5d + 3d * T1 + 10d * C1 - 4d * C1 * C1 - 9d * e_2) * D4 / 24d +
(61d + 90d * T1 + 45d * T1 * T1 + 298 * C1 - 3d * C1 * C1 - 252d * e_2) * D6 / 720d); // (8-17)
var dLambda = (D - (1d + 2d * T1 + C1) * D3 / 6d +
(5d - 2d * C1 - 3d * C1 * C1 + 28d * T1 + 24d * T1 * T1 + 8d * e_2) * D5 / 120d) / cosPhi1; // (8-18)
// γ, https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Convergence
//
return Math.Atan(Math.Tan(dLambda) * Math.Sin(phi)) * 180d / Math.PI;
return EquatorialRadius * (
(1d - e2 / 4d - e4 * 3d / 64d - e6 * 5d / 256d) * phi -
(e2 * 3d / 8d + e4 * 3d / 32d + e6 * 45d / 1024d) * Math.Sin(2d * phi) +
(e4 * 15d / 256d + e6 * 45d / 1024d) * Math.Sin(4d * phi) -
e6 * 35d / 3072d * Math.Sin(6d * phi)); // (3-21)
}
}
}

View file

@ -190,7 +190,8 @@ namespace MapControl
var xMin = -180d * MapProjection.Wgs84MeterPerDegree;
var xMax = 180d * MapProjection.Wgs84MeterPerDegree;
if (bbox.X >= xMin && bbox.X + bbox.Width <= xMax || !ParentMap.MapProjection.IsNormalCylindrical)
if (!ParentMap.MapProjection.IsNormalCylindrical ||
bbox.X >= xMin && bbox.X + bbox.Width <= xMax)
{
var uri = GetMapRequestUri(bbox);

View file

@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
@ -87,17 +86,9 @@ namespace MapControl
}
}
private static PathFigure CreatePolylineFigure(IEnumerable<Point> points)
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
{
var figure = new PathFigure
{
StartPoint = points.First(),
IsClosed = false,
IsFilled = false
};
figure.Segments.Add(new PolyLineSegment(points.Skip(1), true));
return figure;
return new PolyLineSegment(points, true);
}
}
}

View file

@ -1,6 +1,5 @@
using Windows.Foundation;
using System.Collections.Generic;
using System.Linq;
#if UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
@ -112,24 +111,16 @@ namespace MapControl
base.OnViewportChanged(e);
}
private static PathFigure CreatePolylineFigure(IEnumerable<Point> points)
private static PolyLineSegment CreatePolyLineSegment(IEnumerable<Point> points)
{
var figure = new PathFigure
{
StartPoint = points.First(),
IsClosed = false,
IsFilled = false
};
var polyline = new PolyLineSegment();
foreach (var p in points.Skip(1))
foreach (var p in points)
{
polyline.Points.Add(p);
}
figure.Segments.Add(polyline);
return figure;
return polyline;
}
}
}

View file

@ -52,10 +52,13 @@ namespace MapControl.Projections
var projection = field.Projection ??
throw new ArgumentException("CoordinateSystem.Projection must not be null.", nameof(value));
IsNormalCylindrical = projection.Name.StartsWith("Mercator") || projection.Name.Contains("Pseudo-Mercator");
var ellipsoid = field.GeographicCoordinateSystem.HorizontalDatum.Ellipsoid;
var transformFactory = new CoordinateTransformationFactory();
CrsId = !string.IsNullOrEmpty(field.Authority) && field.AuthorityCode > 0
? $"{field.Authority}:{field.AuthorityCode}"
: string.Empty;
LocationToMapTransform = transformFactory
.CreateFromCoordinateSystems(GeographicCoordinateSystem.WGS84, field)
.MathTransform;
@ -64,14 +67,9 @@ namespace MapControl.Projections
.CreateFromCoordinateSystems(field, GeographicCoordinateSystem.WGS84)
.MathTransform;
CrsId = !string.IsNullOrEmpty(field.Authority) && field.AuthorityCode > 0
? $"{field.Authority}:{field.AuthorityCode}"
: string.Empty;
var ellipsoid = field.GeographicCoordinateSystem.HorizontalDatum.Ellipsoid;
if (projection.Name.Contains("Pseudo-Mercator"))
{
IsNormalCylindrical = true;
FallbackProjection = new MapControl.WebMercatorProjection
{
EquatorialRadius = ellipsoid.SemiMajorAxis
@ -79,6 +77,7 @@ namespace MapControl.Projections
}
else if (projection.Name.StartsWith("Mercator"))
{
IsNormalCylindrical = true;
FallbackProjection = new MapControl.WorldMercatorProjection
{
EquatorialRadius = ellipsoid.SemiMajorAxis,
@ -106,7 +105,7 @@ namespace MapControl.Projections
ScaleFactor = projection.GetParameter("scale_factor").Value,
FalseEasting = projection.GetParameter("false_easting").Value,
FalseNorthing = projection.GetParameter("false_northing").Value,
Hemisphere = projection.GetParameter("latitude_of_origin").Value >= 0 ? Hemisphere.North : Hemisphere.South
LatitudeOfOrigin = projection.GetParameter("latitude_of_origin").Value
};
}
}

View file

@ -24,7 +24,7 @@ namespace MapControl.Projections
{ 29193, WktConstants.ProjCsSad69Utm23S },
};
public override MapProjection GetProjection(string crsId)
protected override MapProjection CreateProjection(string crsId)
{
return crsId switch
{
@ -32,11 +32,11 @@ namespace MapControl.Projections
MapControl.WorldMercatorProjection.DefaultCrsId => new WorldMercatorProjection(),
MapControl.Wgs84UpsNorthProjection.DefaultCrsId => new Wgs84UpsNorthProjection(),
MapControl.Wgs84UpsSouthProjection.DefaultCrsId => new Wgs84UpsSouthProjection(),
_ => GetProjectionFromEpsgCode(crsId) ?? base.GetProjection(crsId)
_ => base.CreateProjection(crsId)
};
}
public override MapProjection GetProjection(int epsgCode)
protected override MapProjection CreateProjection(int epsgCode)
{
if (CoordinateSystemWkts.TryGetValue(epsgCode, out string wkt))
{
@ -57,7 +57,7 @@ namespace MapControl.Projections
and <= MapControl.Wgs84UtmProjection.LastZoneNorthEpsgCode => new Wgs84UtmProjection(c % 100, Hemisphere.North),
var c when c is >= MapControl.Wgs84UtmProjection.FirstZoneSouthEpsgCode
and <= MapControl.Wgs84UtmProjection.LastZoneSouthEpsgCode => new Wgs84UtmProjection(c % 100, Hemisphere.South),
_ => base.GetProjection(epsgCode)
_ => base.CreateProjection(epsgCode)
};
}
}