diff --git a/src/Nonemm.App/Configuration/Settings.cs b/src/Nonemm.App/Configuration/Settings.cs
index 43c5f35..0072257 100644
--- a/src/Nonemm.App/Configuration/Settings.cs
+++ b/src/Nonemm.App/Configuration/Settings.cs
@@ -356,6 +356,38 @@ public sealed record StoredStation
{
public string Callsign { get; init; } = "";
+ public string Address1 { get; init; } = "";
+
+ public string Address2 { get; init; } = "";
+
+ public string City { get; init; } = "";
+
+ public string Zip { get; init; } = "";
+
+ /// The country written out, as it goes on an award application. The prefix
+ /// the contest rules compare against is `CountryPrefix`.
+ public string Country { get; init; } = "";
+
+ public string License { get; init; } = "";
+
+ public double Latitude { get; init; }
+
+ public double Longitude { get; init; }
+
+ /// The radio, as N1MM's "Station" box asks for it.
+ public string Rig { get; init; } = "";
+
+ public string Antenna { get; init; } = "";
+
+ public string AntennaHeight { get; init; } = "";
+
+ /// Height above sea level.
+ public string AboveSeaLevel { get; init; } = "";
+
+ public string RoverQth { get; init; } = "";
+
+ public string Email { get; init; } = "";
+
public int CqZone { get; init; }
public int ItuZone { get; init; }
@@ -401,5 +433,7 @@ public sealed record StoredStation
Club = Club,
Check = Check,
Precedence = Precedence,
+ Latitude = Latitude,
+ Longitude = Longitude,
};
}
diff --git a/src/Nonemm.App/Dialogs/StationDialog.axaml b/src/Nonemm.App/Dialogs/StationDialog.axaml
index ed5db46..bca2cb4 100644
--- a/src/Nonemm.App/Dialogs/StationDialog.axaml
+++ b/src/Nonemm.App/Dialogs/StationDialog.axaml
@@ -1,56 +1,123 @@
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Nonemm.App/Dialogs/StationDialog.axaml.cs b/src/Nonemm.App/Dialogs/StationDialog.axaml.cs
index 3df602a..4a3daba 100644
--- a/src/Nonemm.App/Dialogs/StationDialog.axaml.cs
+++ b/src/Nonemm.App/Dialogs/StationDialog.axaml.cs
@@ -1,4 +1,6 @@
+using System.Globalization;
using Avalonia.Controls;
+using Nonemm.Core;
using Avalonia.Interactivity;
using Nonemm.App.Configuration;
@@ -8,47 +10,120 @@ namespace Nonemm.App.Dialogs;
/// rules compare a worked station against.
public sealed partial class StationDialog : Window
{
+ private readonly StoredStation station;
+
public StationDialog(StoredStation station)
{
+ this.station = station;
InitializeComponent();
+ LatitudeSignBox.ItemsSource = new[] { "N", "S" };
+ LongitudeSignBox.ItemsSource = new[] { "E", "W" };
+
CallsignBox.Text = station.Callsign;
+ NameBox.Text = station.Name;
+ AddressBox.Text = station.Address1;
+ Address2Box.Text = station.Address2;
+ CityBox.Text = station.City;
+ StateBox.Text = station.State;
+ ZipBox.Text = station.Zip;
+ CountryBox.Text = station.Country;
+ GridBox.Text = station.GridSquare;
CqZoneBox.Text = station.CqZone.ToString();
ItuZoneBox.Text = station.ItuZone.ToString();
- ContinentBox.Text = station.Continent;
- CountryBox.Text = station.CountryPrefix;
- GridBox.Text = station.GridSquare;
- StateBox.Text = station.State;
- ProvinceBox.Text = station.Province;
- SectionBox.Text = station.ArrlSection;
- NameBox.Text = station.Name;
+ LicenseBox.Text = station.License;
+ LatitudeBox.Text = Magnitude(station.Latitude);
+ LatitudeSignBox.SelectedIndex = station.Latitude < 0 ? 1 : 0;
+ LongitudeBox.Text = Magnitude(station.Longitude);
+ LongitudeSignBox.SelectedIndex = station.Longitude < 0 ? 1 : 0;
+ RigBox.Text = station.Rig;
PowerBox.Text = station.Power;
+ AntennaBox.Text = station.Antenna;
+ AntennaHeightBox.Text = station.AntennaHeight;
+ AboveSeaLevelBox.Text = station.AboveSeaLevel;
+ SectionBox.Text = station.ArrlSection;
+ RoverQthBox.Text = station.RoverQth;
ClubBox.Text = station.Club;
- CheckBox.Text = station.Check.ToString();
- PrecedenceBox.Text = station.Precedence;
- CountyBox.Text = station.County;
+ EmailBox.Text = station.Email;
+
+ GridBox.TextChanged += (_, _) => FillPositionFromGrid();
+ LatitudeBox.TextChanged += (_, _) => FillGridFromPosition();
+ LongitudeBox.TextChanged += (_, _) => FillGridFromPosition();
+ LatitudeSignBox.SelectionChanged += (_, _) => FillGridFromPosition();
+ LongitudeSignBox.SelectionChanged += (_, _) => FillGridFromPosition();
}
+ /// N1MM fills each of the grid square and the position in from the other.
+ /// Which one is being typed into decides the direction: the events that
+ /// report a box being written to arrive too late to tell them apart.
+ private void FillPositionFromGrid()
+ {
+ if (!GridBox.IsFocused || !GridSquare.TryParse(GridBox.Text ?? "", out GridSquare grid))
+ {
+ return;
+ }
+ LatitudeBox.Text = Magnitude(grid.Latitude);
+ LatitudeSignBox.SelectedIndex = grid.Latitude < 0 ? 1 : 0;
+ LongitudeBox.Text = Magnitude(grid.Longitude);
+ LongitudeSignBox.SelectedIndex = grid.Longitude < 0 ? 1 : 0;
+ }
- private void OnSave(object? sender, RoutedEventArgs e) => Close(new StoredStation
+ private void FillGridFromPosition()
+ {
+ bool typed = LatitudeBox.IsFocused || LongitudeBox.IsFocused ||
+ LatitudeSignBox.IsFocused || LongitudeSignBox.IsFocused;
+ if (!typed || (LatitudeBox.Text ?? "").Trim().Length == 0 || (LongitudeBox.Text ?? "").Trim().Length == 0)
+ {
+ return;
+ }
+ double latitude = Signed(LatitudeBox, LatitudeSignBox);
+ double longitude = Signed(LongitudeBox, LongitudeSignBox);
+ if (Math.Abs(latitude) > 90.0 || Math.Abs(longitude) > 180.0)
+ {
+ return;
+ }
+ GridBox.Text = GridSquare.From(latitude, longitude);
+ }
+
+ /// The boxes over the record the dialog was opened with, so the fields
+ /// N1MM's form does not show keep the value they had.
+ private void OnSave(object? sender, RoutedEventArgs e) => Close(station with
{
Callsign = (CallsignBox.Text ?? "").Trim().ToUpperInvariant(),
+ Name = (NameBox.Text ?? "").Trim(),
+ Address1 = (AddressBox.Text ?? "").Trim(),
+ Address2 = (Address2Box.Text ?? "").Trim(),
+ City = (CityBox.Text ?? "").Trim(),
+ State = (StateBox.Text ?? "").Trim().ToUpperInvariant(),
+ Zip = (ZipBox.Text ?? "").Trim(),
+ Country = (CountryBox.Text ?? "").Trim(),
+ GridSquare = (GridBox.Text ?? "").Trim(),
CqZone = Number(CqZoneBox),
ItuZone = Number(ItuZoneBox),
- Continent = (ContinentBox.Text ?? "").Trim().ToUpperInvariant(),
- CountryPrefix = (CountryBox.Text ?? "").Trim().ToUpperInvariant(),
- GridSquare = (GridBox.Text ?? "").Trim(),
- State = (StateBox.Text ?? "").Trim().ToUpperInvariant(),
- Province = (ProvinceBox.Text ?? "").Trim().ToUpperInvariant(),
- ArrlSection = (SectionBox.Text ?? "").Trim().ToUpperInvariant(),
- Name = (NameBox.Text ?? "").Trim(),
+ License = (LicenseBox.Text ?? "").Trim(),
+ Latitude = Signed(LatitudeBox, LatitudeSignBox),
+ Longitude = Signed(LongitudeBox, LongitudeSignBox),
+ Rig = (RigBox.Text ?? "").Trim(),
Power = (PowerBox.Text ?? "").Trim(),
+ Antenna = (AntennaBox.Text ?? "").Trim(),
+ AntennaHeight = (AntennaHeightBox.Text ?? "").Trim(),
+ AboveSeaLevel = (AboveSeaLevelBox.Text ?? "").Trim(),
+ ArrlSection = (SectionBox.Text ?? "").Trim().ToUpperInvariant(),
+ RoverQth = (RoverQthBox.Text ?? "").Trim().ToUpperInvariant(),
Club = (ClubBox.Text ?? "").Trim(),
- Check = Number(CheckBox),
- Precedence = (PrecedenceBox.Text ?? "").Trim().ToUpperInvariant(),
- County = (CountyBox.Text ?? "").Trim().ToUpperInvariant(),
+ Email = (EmailBox.Text ?? "").Trim(),
});
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
private static int Number(TextBox box) => int.TryParse(box.Text, out int value) ? value : 0;
+
+ private static string Magnitude(double degrees) =>
+ degrees == 0 ? "" : Math.Abs(degrees).ToString("0.####", CultureInfo.InvariantCulture);
+
+ private static double Signed(TextBox box, ComboBox sign)
+ {
+ double degrees = double.TryParse(
+ box.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out double value) ? Math.Abs(value) : 0;
+ return sign.SelectedIndex == 1 ? -degrees : degrees;
+ }
}
diff --git a/src/Nonemm.App/Windows/EntryWindow.Menu.cs b/src/Nonemm.App/Windows/EntryWindow.Menu.cs
index 14ac143..cf0059a 100644
--- a/src/Nonemm.App/Windows/EntryWindow.Menu.cs
+++ b/src/Nonemm.App/Windows/EntryWindow.Menu.cs
@@ -4,6 +4,7 @@ using Avalonia.Platform.Storage;
using Nonemm.App.Configuration;
using Nonemm.App.Dialogs;
using Nonemm.Core;
+using Nonemm.Core.Country;
using Nonemm.Formats.Adif;
using Nonemm.Formats.Cabrillo;
using Nonemm.Session;
@@ -244,7 +245,7 @@ public sealed partial class EntryWindow
StoredStation? updated = await dialog.ShowDialog(this);
if (updated is not null)
{
- session.Save(session.Settings with { Station = updated });
+ session.Save(session.Settings with { Station = FromCall(updated) });
if (Logging is not null)
{
session.OpenContest(Logging.Instance.ContestNumber);
@@ -252,6 +253,18 @@ public sealed partial class EntryWindow
}
}
+ /// N1MM's station form has no continent or country box: it looks both up
+ /// from the callsign in the country file.
+ private StoredStation FromCall(StoredStation station)
+ {
+ CountryLookup? country = session.Countries?.Find(station.Callsign);
+ return country is null ? station : station with
+ {
+ Continent = country.Continent,
+ CountryPrefix = country.Entity.PrimaryPrefix,
+ };
+ }
+
private async void OnRadioSettings(object? sender, RoutedEventArgs e)
{
RadioDialog dialog = new(session.Settings);
diff --git a/src/Nonemm.Core/GridSquare.cs b/src/Nonemm.Core/GridSquare.cs
index 458a4fc..c7810b1 100644
--- a/src/Nonemm.Core/GridSquare.cs
+++ b/src/Nonemm.Core/GridSquare.cs
@@ -68,6 +68,24 @@ public readonly record struct GridSquare
return true;
}
+ /// The six-character locator of a point, which is what N1MM fills the grid
+ /// square box in with.
+ public static string From(double latitude, double longitude)
+ {
+ double lat = Math.Clamp(latitude, -90.0, 90.0) + 90.0;
+ double lon = Math.Clamp(longitude, -180.0, 180.0) + 180.0;
+ char[] text =
+ [
+ (char)('A' + (int)(lon / 20.0)),
+ (char)('A' + (int)(lat / 10.0)),
+ (char)('0' + (int)(lon % 20.0 / 2.0)),
+ (char)('0' + (int)(lat % 10.0)),
+ (char)('a' + (int)(lon % 2.0 * 12.0)),
+ (char)('a' + (int)(lat % 1.0 * 24.0)),
+ ];
+ return new string(text);
+ }
+
/// Great-circle distance in kilometres.
public double DistanceTo(GridSquare other) =>
DistanceKm(Latitude, Longitude, other.Latitude, other.Longitude);
diff --git a/tests/Nonemm.Core.Tests/GridSquareTests.cs b/tests/Nonemm.Core.Tests/GridSquareTests.cs
index b0f1834..0912e05 100644
--- a/tests/Nonemm.Core.Tests/GridSquareTests.cs
+++ b/tests/Nonemm.Core.Tests/GridSquareTests.cs
@@ -43,4 +43,15 @@ public class GridSquareTests
Assert.True(GridSquare.TryParse("JP90", out GridSquare north));
Assert.Equal(0, south.BearingTo(north), 6);
}
+
+ [Theory]
+ [InlineData("JN88md")]
+ [InlineData("FN31pr")]
+ [InlineData("AA00aa")]
+ [InlineData("RR99xx")]
+ public void LocatorOfASquareCentreIsThatSquare(string locator)
+ {
+ Assert.True(GridSquare.TryParse(locator, out GridSquare grid));
+ Assert.Equal(locator, GridSquare.From(grid.Latitude, grid.Longitude));
+ }
}