40 lines
1.6 KiB
C#
40 lines
1.6 KiB
C#
using System.Numerics;
|
|
|
|
namespace TheLivingWorld.Core.Geo;
|
|
|
|
/// <summary>
|
|
/// Flattens WGS84 coordinates onto a local metric plane centred on an origin: X grows east, Y grows north,
|
|
/// both in metres. This is an equirectangular projection with the scale factor frozen at the origin latitude,
|
|
/// which stays sub-metre accurate across the 5-20 km worlds this game generates and — unlike Web Mercator —
|
|
/// keeps distances usable directly as game units.
|
|
/// </summary>
|
|
public sealed class LocalProjection
|
|
{
|
|
public const double EarthRadiusMeters = 6378137.0;
|
|
public const double MetersPerDegreeLatitude = EarthRadiusMeters * Math.PI / 180.0;
|
|
|
|
private readonly double _metersPerDegreeLongitude;
|
|
|
|
public LocalProjection(GeoPoint origin)
|
|
{
|
|
if (!origin.IsValid)
|
|
throw new ArgumentOutOfRangeException(nameof(origin), origin, "Origin is not a valid WGS84 coordinate.");
|
|
|
|
Origin = origin;
|
|
var cosLat = Math.Cos(origin.Latitude * Math.PI / 180.0);
|
|
_metersPerDegreeLongitude = MetersPerDegreeLatitude * Math.Max(cosLat, 1e-6);
|
|
}
|
|
|
|
public GeoPoint Origin { get; }
|
|
|
|
public Vector2 Project(GeoPoint point) => Project(point.Latitude, point.Longitude);
|
|
|
|
public Vector2 Project(double latitude, double longitude) => new(
|
|
(float)((longitude - Origin.Longitude) * _metersPerDegreeLongitude),
|
|
(float)((latitude - Origin.Latitude) * MetersPerDegreeLatitude));
|
|
|
|
public GeoPoint Unproject(Vector2 local) => new(
|
|
Origin.Latitude + local.Y / MetersPerDegreeLatitude,
|
|
Origin.Longitude + local.X / _metersPerDegreeLongitude);
|
|
}
|