namespace Nonemm.Core; /// A Maidenhead locator, and the great-circle maths contests need from it. public readonly record struct GridSquare { private GridSquare(string text, double latitude, double longitude) { Text = text; Latitude = latitude; Longitude = longitude; } /// The locator as given, upper case for the field and square, lower for the /// subsquare, which is how locators are written. public string Text { get; } /// The centre of the square, in degrees, longitude east-positive. public double Latitude { get; } public double Longitude { get; } public static bool TryParse(string text, out GridSquare grid) { grid = default; string t = text.Trim(); if (t.Length is not (4 or 6 or 8) || t.Length % 2 != 0) { return false; } if (!InRange(t[0], 'A', 'R') || !InRange(t[1], 'A', 'R') || !char.IsAsciiDigit(t[2]) || !char.IsAsciiDigit(t[3])) { return false; } double longitude = ((Upper(t[0]) - 'A') * 20.0) + ((t[2] - '0') * 2.0); double latitude = ((Upper(t[1]) - 'A') * 10.0) + (t[3] - '0'); double longitudeSize = 2.0; double latitudeSize = 1.0; if (t.Length >= 6) { if (!InRange(t[4], 'A', 'X') || !InRange(t[5], 'A', 'X')) { return false; } longitude += (Upper(t[4]) - 'A') * (2.0 / 24.0); latitude += (Upper(t[5]) - 'A') * (1.0 / 24.0); longitudeSize = 2.0 / 24.0; latitudeSize = 1.0 / 24.0; } if (t.Length == 8) { if (!char.IsAsciiDigit(t[6]) || !char.IsAsciiDigit(t[7])) { return false; } longitude += (t[6] - '0') * (2.0 / 240.0); latitude += (t[7] - '0') * (1.0 / 240.0); longitudeSize = 2.0 / 240.0; latitudeSize = 1.0 / 240.0; } grid = new GridSquare( Normalize(t), latitude + (latitudeSize / 2.0) - 90.0, longitude + (longitudeSize / 2.0) - 180.0); return true; } /// Great-circle distance in kilometres. public double DistanceTo(GridSquare other) => DistanceKm(Latitude, Longitude, other.Latitude, other.Longitude); /// Initial bearing in degrees, 0 at north. public double BearingTo(GridSquare other) => Bearing(Latitude, Longitude, other.Latitude, other.Longitude); public override string ToString() => Text; public static double DistanceKm(double lat1, double lon1, double lat2, double lon2) { const double earthRadiusKm = 6371.0; double dLat = Radians(lat2 - lat1); double dLon = Radians(lon2 - lon1); double a = (Math.Sin(dLat / 2) * Math.Sin(dLat / 2)) + (Math.Cos(Radians(lat1)) * Math.Cos(Radians(lat2)) * Math.Sin(dLon / 2) * Math.Sin(dLon / 2)); return earthRadiusKm * 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); } public static double Bearing(double lat1, double lon1, double lat2, double lon2) { double dLon = Radians(lon2 - lon1); double y = Math.Sin(dLon) * Math.Cos(Radians(lat2)); double x = (Math.Cos(Radians(lat1)) * Math.Sin(Radians(lat2))) - (Math.Sin(Radians(lat1)) * Math.Cos(Radians(lat2)) * Math.Cos(dLon)); return ((Math.Atan2(y, x) * 180.0 / Math.PI) + 360.0) % 360.0; } private static double Radians(double degrees) => degrees * Math.PI / 180.0; private static bool InRange(char c, char low, char high) { char u = Upper(c); return u >= low && u <= high; } private static char Upper(char c) => char.ToUpperInvariant(c); private static string Normalize(string t) => t.Length <= 4 ? t.ToUpperInvariant() : t[..4].ToUpperInvariant() + t[4..6].ToLowerInvariant() + t[6..].ToUpperInvariant(); }