Frequencies, bands, modes, callsigns, grid squares and the country file live in Nonemm.Core. Nonemm.Contests holds the scoring engine and CQ WW and CQ WPX. Nonemm.Storage writes N1MM's DXLOG schema, and Nonemm.Formats writes Cabrillo 3.0 and reads and writes ADIF. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
3.9 KiB
C#
116 lines
3.9 KiB
C#
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();
|
|
}
|