XAML-Map-Control/MapControl/Shared/MapProjection.cs

209 lines
8 KiB
C#
Raw Normal View History

2025-02-27 18:46:32 +01:00
using System;
2024-05-22 11:25:32 +02:00
#if WPF
using System.Windows;
using System.Windows.Media;
2025-08-19 19:43:02 +02:00
#elif AVALONIA
using Avalonia;
#endif
namespace MapControl
{
/// <summary>
2026-01-24 17:42:00 +01:00
/// Implements a map projection, a transformation between geographic coordinates,
/// i.e. latitude and longitude in degrees, and cartesian map coordinates in meters.
/// </summary>
2025-09-06 13:06:00 +02:00
#if UWP || WINUI
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
#else
[System.ComponentModel.TypeConverter(typeof(MapProjectionConverter))]
#endif
public abstract class MapProjection
{
public const double Wgs84EquatorialRadius = 6378137d;
2018-12-20 21:55:12 +01:00
public const double Wgs84Flattening = 1d / 298.257223563;
public const double Wgs84MeterPerDegree = Wgs84EquatorialRadius * Math.PI / 180d;
2025-09-20 14:01:51 +02:00
public static MapProjectionFactory Factory
{
2025-12-27 21:24:01 +01:00
get => field ??= new MapProjectionFactory();
set;
2025-09-20 14:01:51 +02:00
}
/// <summary>
/// Creates a MapProjection instance from a CRS identifier string.
/// </summary>
public static MapProjection Parse(string crsId)
{
return Factory.GetProjection(crsId);
}
/// <summary>
2026-01-24 17:42:00 +01:00
/// Gets the WMS 1.3.0 CRS identifier.
/// </summary>
2026-01-24 17:42:00 +01:00
public string CrsId { get; protected set; } = "";
/// <summary>
2026-01-24 17:42:00 +01:00
/// Indicates whether the projection is normal cylindrical, see
/// https://en.wikipedia.org/wiki/Map_projection#Normal_cylindrical.
/// </summary>
2026-01-24 17:42:00 +01:00
public bool IsNormalCylindrical { get; protected set; }
/// <summary>
2026-01-24 17:42:00 +01:00
/// The earth ellipsoid semi-major axis, or spherical earth radius respectively, in meters.
/// </summary>
public double EquatorialRadius { get; set; } = Wgs84EquatorialRadius;
public double MeterPerDegree => EquatorialRadius * Math.PI / 180d;
private Location center;
private bool updateCenter;
/// <summary>
/// Gets or sets an optional projection center. If the property is set to a non-null value,
/// it overrides the projection center set by MapBase.Center or MapBase.ProjectionCenter.
/// </summary>
public Location Center
{
get => center ??= new Location();
set
{
updateCenter = true;
if (value != null)
{
2026-01-25 11:57:14 +01:00
var longitude = Location.NormalizeLongitude(value.Longitude);
SetCenter(value.LongitudeEquals(longitude) ? value : new Location(value.Latitude, longitude));
updateCenter = false;
}
}
}
protected void EnableCenterUpdates()
{
updateCenter = true;
}
/// <summary>
/// Called by MapBase.UpdateTransform().
/// </summary>
internal void SetCenter(Location value)
{
if (updateCenter)
{
2026-01-25 11:57:14 +01:00
if (center == null || !center.Equals(value))
{
2026-01-25 11:57:14 +01:00
center = value;
CenterChanged();
}
}
}
2026-01-26 19:00:40 +01:00
protected virtual void CenterChanged() { }
/// <summary>
/// Gets the relative scale at the specified geographic coordinates.
/// The returned Matrix represents the local distortion of the map projection.
/// </summary>
2026-01-25 18:04:59 +01:00
public abstract Matrix RelativeScale(double latitude, double longitude);
/// <summary>
/// Transforms geographic coordinates to a Point in projected map coordinates.
/// Returns null when the location can not be transformed.
/// </summary>
public abstract Point? LocationToMap(double latitude, double longitude);
/// <summary>
/// Transforms projected map coordinates to a Location in geographic coordinates.
/// Returns null when the coordinates can not be transformed.
/// </summary>
public abstract Location MapToLocation(double x, double y);
/// <summary>
/// Gets the relative map scale at the specified geographic Location.
/// </summary>
public Matrix RelativeScale(Location location) => RelativeScale(location.Latitude, location.Longitude);
/// <summary>
2022-11-05 17:32:29 +01:00
/// Transforms a Location in geographic coordinates to a Point in projected map coordinates.
/// Returns null when the Location can not be transformed.
/// </summary>
public Point? LocationToMap(Location location) => LocationToMap(location.Latitude, location.Longitude);
/// <summary>
2022-11-05 17:32:29 +01:00
/// Transforms a Point in projected map coordinates to a Location in geographic coordinates.
2022-03-04 22:28:18 +01:00
/// Returns null when the Point can not be transformed.
/// </summary>
public Location MapToLocation(Point point) => MapToLocation(point.X, point.Y);
/// <summary>
/// Transforms a BoundingBox in geographic coordinates to a Rect in projected map coordinates.
/// Returns null when the BoundingBox can not be transformed.
/// </summary>
2026-01-25 18:04:59 +01:00
public Rect? BoundingBoxToMap(BoundingBox boundingBox)
{
2026-01-26 19:00:40 +01:00
var sw = LocationToMap(boundingBox.South, boundingBox.West);
var ne = LocationToMap(boundingBox.North, boundingBox.East);
2022-12-08 10:37:56 +01:00
2026-01-26 19:00:40 +01:00
return sw.HasValue && ne.HasValue ? new Rect(sw.Value, ne.Value) : null;
}
/// <summary>
/// Transforms a Rect in projected map coordinates to a BoundingBox in geographic coordinates.
2026-01-25 18:04:59 +01:00
/// Returns null when the Rect can not be transformed.
/// </summary>
2026-01-25 18:04:59 +01:00
public BoundingBox MapToBoundingBox(Rect rect)
{
2026-01-26 19:00:40 +01:00
var sw = MapToLocation(rect.X, rect.Y);
var ne = MapToLocation(rect.X + rect.Width, rect.Y + rect.Height);
2026-01-26 19:00:40 +01:00
return sw != null && ne != null ? new BoundingBox(sw, ne) : null;
}
2024-09-09 16:44:45 +02:00
/// <summary>
/// Transforms a LatLonBox in geographic coordinates to a rotated Rect in projected map coordinates.
2026-01-26 19:00:40 +01:00
/// Returns (null, 0d) when the LatLonBox can not be transformed.
2024-09-09 16:44:45 +02:00
/// </summary>
2026-01-25 18:04:59 +01:00
public (Rect?, double) LatLonBoxToMap(LatLonBox latLonBox)
2024-09-09 16:44:45 +02:00
{
2025-12-30 08:27:52 +01:00
Rect? rect = null;
var rotation = 0d;
2026-01-26 19:00:40 +01:00
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)
2024-09-09 16:44:45 +02:00
{
2026-01-26 19:00:40 +01:00
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;
2024-09-09 16:44:45 +02:00
2025-12-30 08:27:52 +01:00
rect = new Rect(x, y, width, height);
2026-01-26 19:00:40 +01:00
// Additional rotation from the slope of the line segment west-east.
//
rotation = (latLonBox.Rotation + Math.Atan2(dy1, dx1) * 180d / Math.PI) % 360d;
2024-09-09 16:44:45 +02:00
}
2025-12-30 08:27:52 +01:00
return (rect, rotation);
2024-09-09 16:44:45 +02:00
}
2025-09-06 13:06:00 +02:00
public override string ToString()
{
return CrsId;
}
}
}