2025-02-27 18:46:32 +01:00
|
|
|
|
using System;
|
2024-05-22 11:25:32 +02:00
|
|
|
|
#if WPF
|
2017-06-25 23:05:48 +02:00
|
|
|
|
using System.Windows;
|
2026-01-20 09:48:16 +01:00
|
|
|
|
using System.Windows.Media;
|
2025-08-19 19:43:02 +02:00
|
|
|
|
#elif AVALONIA
|
|
|
|
|
|
using Avalonia;
|
2017-06-25 23:05:48 +02:00
|
|
|
|
#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.
|
2026-02-01 01:42:24 +01:00
|
|
|
|
/// See https://en.wikipedia.org/wiki/Map_projection.
|
2017-06-25 23:05:48 +02:00
|
|
|
|
/// </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
|
2026-01-24 22:41:25 +01:00
|
|
|
|
public abstract class MapProjection
|
2017-06-25 23:05:48 +02:00
|
|
|
|
{
|
2018-02-09 17:43:47 +01:00
|
|
|
|
public const double Wgs84EquatorialRadius = 6378137d;
|
2018-12-20 21:55:12 +01:00
|
|
|
|
public const double Wgs84Flattening = 1d / 298.257223563;
|
2026-01-20 15:48:30 +01:00
|
|
|
|
public const double Wgs84MeterPerDegree = Wgs84EquatorialRadius * Math.PI / 180d;
|
2026-01-09 08:13:07 +01:00
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:06:50 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Creates a MapProjection instance from a CRS identifier string.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static MapProjection Parse(string crsId)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Factory.GetProjection(crsId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-26 19:08:20 +01:00
|
|
|
|
/// <summary>
|
2026-01-24 17:42:00 +01:00
|
|
|
|
/// Gets the WMS 1.3.0 CRS identifier.
|
2017-06-25 23:05:48 +02:00
|
|
|
|
/// </summary>
|
2026-02-01 01:42:24 +01:00
|
|
|
|
public string CrsId { get; protected set; }
|
2017-06-25 23:05:48 +02:00
|
|
|
|
|
|
|
|
|
|
/// <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.
|
2017-06-25 23:05:48 +02:00
|
|
|
|
/// </summary>
|
2026-01-24 17:42:00 +01:00
|
|
|
|
public bool IsNormalCylindrical { get; protected set; }
|
2017-06-25 23:05:48 +02:00
|
|
|
|
|
2026-02-01 01:42:24 +01:00
|
|
|
|
public double EquatorialRadius { get; set; } = Wgs84EquatorialRadius;
|
|
|
|
|
|
public double CentralMeridian { get; set; }
|
|
|
|
|
|
public double LatitudeOfOrigin { get; set; }
|
|
|
|
|
|
|
2026-01-24 14:58:16 +01:00
|
|
|
|
/// <summary>
|
2026-02-01 01:42:24 +01:00
|
|
|
|
/// 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.
|
2026-01-24 14:58:16 +01:00
|
|
|
|
/// </summary>
|
2026-02-01 01:42:24 +01:00
|
|
|
|
public virtual double GridConvergence(double latitude, double longitude) => 0d;
|
2026-01-24 14:58:16 +01:00
|
|
|
|
|
2019-04-05 19:13:58 +02:00
|
|
|
|
/// <summary>
|
2026-01-27 22:56:09 +01:00
|
|
|
|
/// Gets the relative transform at the specified geographic coordinates.
|
2026-01-29 21:36:11 +01:00
|
|
|
|
/// The returned Matrix represents the local relative scale and rotation.
|
2017-06-25 23:05:48 +02:00
|
|
|
|
/// </summary>
|
2026-01-27 22:56:09 +01:00
|
|
|
|
public abstract Matrix RelativeTransform(double latitude, double longitude);
|
2025-12-12 21:28:45 +01:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Transforms geographic coordinates to a Point in projected map coordinates.
|
|
|
|
|
|
/// </summary>
|
2026-01-29 16:27:34 +01:00
|
|
|
|
public abstract Point LocationToMap(double latitude, double longitude);
|
2025-12-12 21:28:45 +01:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Transforms projected map coordinates to a Location in geographic coordinates.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public abstract Location MapToLocation(double x, double y);
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-01-27 22:56:09 +01:00
|
|
|
|
/// Gets the relative transform at the specified geographic Location.
|
2025-12-12 21:28:45 +01:00
|
|
|
|
/// </summary>
|
2026-01-27 22:56:09 +01:00
|
|
|
|
public Matrix RelativeTransform(Location location) => RelativeTransform(location.Latitude, location.Longitude);
|
2017-06-25 23:05:48 +02:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2022-11-05 17:32:29 +01:00
|
|
|
|
/// Transforms a Location in geographic coordinates to a Point in projected map coordinates.
|
2017-06-25 23:05:48 +02:00
|
|
|
|
/// </summary>
|
2026-01-29 16:27:34 +01:00
|
|
|
|
public Point LocationToMap(Location location) => LocationToMap(location.Latitude, location.Longitude);
|
2017-06-25 23:05:48 +02:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2022-11-05 17:32:29 +01:00
|
|
|
|
/// Transforms a Point in projected map coordinates to a Location in geographic coordinates.
|
2017-06-25 23:05:48 +02:00
|
|
|
|
/// </summary>
|
2025-12-12 21:28:45 +01:00
|
|
|
|
public Location MapToLocation(Point point) => MapToLocation(point.X, point.Y);
|
2017-06-25 23:05:48 +02:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-01-29 16:27:34 +01:00
|
|
|
|
/// Transforms a BoundingBox in geographic coordinates to a Rect in projected map coordinates
|
2026-01-30 22:26:51 +01:00
|
|
|
|
/// with an optional rotation angle in degrees for non-normal-cylindrical projections.
|
2017-06-25 23:05:48 +02:00
|
|
|
|
/// </summary>
|
2026-01-29 21:36:11 +01:00
|
|
|
|
public (Rect, double) BoundingBoxToMap(BoundingBox boundingBox)
|
2017-06-25 23:05:48 +02:00
|
|
|
|
{
|
2026-01-29 16:27:34 +01:00
|
|
|
|
Rect rect;
|
2026-01-29 21:36:11 +01:00
|
|
|
|
var rotation = 0d;
|
|
|
|
|
|
var southWest = LocationToMap(boundingBox.South, boundingBox.West);
|
|
|
|
|
|
var northEast = LocationToMap(boundingBox.North, boundingBox.East);
|
2022-12-08 10:37:56 +01:00
|
|
|
|
|
2026-01-29 16:27:34 +01:00
|
|
|
|
if (IsNormalCylindrical)
|
2026-01-27 20:12:57 +01:00
|
|
|
|
{
|
2026-01-29 16:27:34 +01:00
|
|
|
|
rect = new Rect(southWest.X, southWest.Y, northEast.X - southWest.X, northEast.Y - southWest.Y);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-01-29 21:36:11 +01:00
|
|
|
|
var southEast = LocationToMap(boundingBox.South, boundingBox.East);
|
|
|
|
|
|
var northWest = LocationToMap(boundingBox.North, boundingBox.West);
|
|
|
|
|
|
var south = new Point((southWest.X + southEast.X) / 2d, (southWest.Y + southEast.Y) / 2d);
|
|
|
|
|
|
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;
|
2026-02-01 01:42:24 +01:00
|
|
|
|
var dxW = northWest.X - southWest.X;
|
|
|
|
|
|
var dyW = northWest.Y - southWest.Y;
|
|
|
|
|
|
var dxE = northEast.X - southEast.X;
|
|
|
|
|
|
var dyE = northEast.Y - southEast.Y;
|
2026-01-29 21:36:11 +01:00
|
|
|
|
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;
|
2026-02-01 01:42:24 +01:00
|
|
|
|
var height = (Math.Sqrt(dxW * dxW + dyW * dyW) + Math.Sqrt(dxE * dxE + dyE * dyE)) / 2d;
|
2026-01-29 21:36:11 +01:00
|
|
|
|
|
|
|
|
|
|
rect = new Rect(centerX - width / 2d, centerY - height / 2d, width, height);
|
2026-02-01 01:42:24 +01:00
|
|
|
|
|
|
|
|
|
|
rotation = -GridConvergence( // invert direction for RotateTransform
|
|
|
|
|
|
(boundingBox.South + boundingBox.North) / 2d,
|
|
|
|
|
|
(boundingBox.West + boundingBox.East) / 2d);
|
2026-01-27 20:12:57 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 21:36:11 +01:00
|
|
|
|
return (rect, rotation);
|
2017-06-25 23:05:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-06 13:06:00 +02:00
|
|
|
|
public override string ToString()
|
|
|
|
|
|
{
|
|
|
|
|
|
return CrsId;
|
|
|
|
|
|
}
|
2017-06-25 23:05:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|