using System.Numerics; namespace TheLivingWorld.Core.Geo; /// /// 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. /// 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); }