Read call history files, and fill the exchange from them
N1MM's format, so a file published for a contest is read as it is: # comments, !!Order!! naming the columns of the lines after it, semicolons in preference to commas so user text can hold a comma, and a default column order for a file that names none. The directives that change what is stored are acted on; !!Order!! can appear again part way through, because these files are usually several files stuck together. Leaving the callsign box fills what the file knows into the exchange boxes that are still empty. A box with something in it is left alone: what the station actually sends beats what somebody published months ago. The zone comes from the CqZone or the ITUZone column depending on which the contest asks for, and a file with no Sect falls back to State. The check window's two missing columns are in. History lists matching calls from the file. Exchange is not about callsigns at all: it offers the values the exchange box with the cursor in it can hold, which is what N1MM's exchange pane does — it searches the contest's list of valid exchanges, not the log. CheckCandidate.Call is now Text, because an exchange value is not a call. The section-validating directives are read and passed over rather than acted on. Acting on them means holding N1MM's section lists and its rules about retired sections, and getting that wrong throws away good data. Running it caught what the unit tests could not: the exchange was filled and scored but the boxes on screen still looked empty, because nothing resynced them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ public sealed class AppSession : IDisposable
|
||||
paths.CreateFolders();
|
||||
Countries = LoadCountryFile(paths.CountryFile);
|
||||
Calls = LoadCallDatabase(paths.CallDatabaseFile);
|
||||
History = LoadCallHistory(settings.CallHistoryFile);
|
||||
Registry = ContestRegistry.FromFolder(paths.UserDefinedContests, out IReadOnlyList<string> problems);
|
||||
UserDefinedContestProblems = problems;
|
||||
}
|
||||
@@ -49,6 +50,10 @@ public sealed class AppSession : IDisposable
|
||||
|
||||
public CallDatabase Calls { get; private set; }
|
||||
|
||||
/// What was published about the stations in this contest. Empty when no
|
||||
/// file is chosen, and then nothing is filled in for the operator.
|
||||
public CallHistory History { get; private set; } = CallHistory.Empty;
|
||||
|
||||
public Bandmap Bandmap { get; } = new();
|
||||
|
||||
/// The contest in progress: one log and one score, however many radios.
|
||||
@@ -133,7 +138,8 @@ public sealed class AppSession : IDisposable
|
||||
?? throw new InvalidOperationException($"no contest numbered {contestNumber} in the log");
|
||||
Contest contest = Registry.Create(instance.ContestName, ModeCategoryOf(instance));
|
||||
SaveDefinition(contest);
|
||||
Logging = new ContestSession(Store, contest, instance, Settings.Station.ToStationInfo(), Countries);
|
||||
Logging = new ContestSession(
|
||||
Store, contest, instance, Settings.Station.ToStationInfo(), Countries, History);
|
||||
positions.Clear();
|
||||
for (int number = 1; number <= PositionCount; number++)
|
||||
{
|
||||
@@ -384,6 +390,7 @@ public sealed class AppSession : IDisposable
|
||||
{
|
||||
Countries = LoadCountryFile(Paths.CountryFile);
|
||||
Calls = LoadCallDatabase(Paths.CallDatabaseFile);
|
||||
History = LoadCallHistory(Settings.CallHistoryFile);
|
||||
Registry = ContestRegistry.FromFolder(Paths.UserDefinedContests, out _);
|
||||
if (Logging is not null)
|
||||
{
|
||||
@@ -424,6 +431,22 @@ public sealed class AppSession : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// A file that will not parse is reported by coming back empty rather than
|
||||
/// stopping the program before a contest.
|
||||
private static CallHistory LoadCallHistory(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return path.Length > 0 && File.Exists(path)
|
||||
? CallHistory.Parse(File.ReadAllText(path))
|
||||
: CallHistory.Empty;
|
||||
}
|
||||
catch (Exception e) when (e is FormatException or IOException)
|
||||
{
|
||||
return CallHistory.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static CallDatabase LoadCallDatabase(string path)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -43,6 +43,10 @@ public sealed record Settings
|
||||
|
||||
public IReadOnlyList<string> NetworkPeers { get; init; } = [];
|
||||
|
||||
/// The call history file for this contest, or empty for none. They are
|
||||
/// published per contest, so this is not a fixed name.
|
||||
public string CallHistoryFile { get; init; } = "";
|
||||
|
||||
/// The serial port of an SO2R box speaking OTRSP, or empty for none. The
|
||||
/// box routes the transmitter and the headphones between the two radios.
|
||||
public string So2rBoxPort { get; init; } = "";
|
||||
|
||||
@@ -11,12 +11,10 @@ namespace Nonemm.App.Windows;
|
||||
public sealed partial class CheckWindow : RefreshableWindow
|
||||
{
|
||||
private readonly AppSession session;
|
||||
private readonly Func<string> typed;
|
||||
|
||||
public CheckWindow(AppSession session, Func<string> typed)
|
||||
public CheckWindow(AppSession session)
|
||||
{
|
||||
this.session = session;
|
||||
this.typed = typed;
|
||||
InitializeComponent();
|
||||
Refresh();
|
||||
}
|
||||
@@ -29,8 +27,14 @@ public sealed partial class CheckWindow : RefreshableWindow
|
||||
{
|
||||
return;
|
||||
}
|
||||
IReadOnlyList<CheckColumn> columns = session.Check.Columns();
|
||||
Columns.ColumnDefinitions.Clear();
|
||||
foreach (CheckColumn _ in columns)
|
||||
{
|
||||
Columns.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star));
|
||||
}
|
||||
int at = 0;
|
||||
foreach (CheckColumn column in session.Check.Columns(typed()))
|
||||
foreach (CheckColumn column in columns)
|
||||
{
|
||||
Control panel = BuildColumn(column);
|
||||
Grid.SetColumn(panel, at++);
|
||||
@@ -52,7 +56,7 @@ public sealed partial class CheckWindow : RefreshableWindow
|
||||
{
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = candidate.Call,
|
||||
Text = candidate.Text,
|
||||
FontFamily = new FontFamily("monospace"),
|
||||
FontSize = 14,
|
||||
Foreground = Verdicts.Colour(candidate.Verdict),
|
||||
@@ -71,6 +75,8 @@ public sealed partial class CheckWindow : RefreshableWindow
|
||||
{
|
||||
CheckSource.Log => "Log",
|
||||
CheckSource.Database => "Master",
|
||||
_ => "Bandmap",
|
||||
CheckSource.History => "History",
|
||||
CheckSource.Bandmap => "Bandmap",
|
||||
_ => "Exchange",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ public sealed partial class EntryWindow
|
||||
|
||||
private void OnShowLog(object? sender, RoutedEventArgs e) => Show(() => new LogWindow(session));
|
||||
|
||||
private void OnShowCheck(object? sender, RoutedEventArgs e) => Show(() => new CheckWindow(session, () => Logging?.Entry.Call ?? ""));
|
||||
private void OnShowCheck(object? sender, RoutedEventArgs e) => Show(() => new CheckWindow(session));
|
||||
|
||||
private void OnShowBandmap(object? sender, RoutedEventArgs e) => Show(() => new BandmapWindow(session, Tune));
|
||||
|
||||
@@ -329,6 +329,27 @@ public sealed partial class EntryWindow
|
||||
BuildFunctionKeys();
|
||||
}
|
||||
|
||||
/// Call history files are published per contest, so the operator points at
|
||||
/// one rather than the program looking in a fixed place.
|
||||
private async void OnCallHistoryFile(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
IReadOnlyList<IStorageFile> files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Call history file",
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = await Folder(session.Paths.SupportFiles),
|
||||
});
|
||||
if (files.FirstOrDefault()?.TryGetLocalPath() is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
session.Save(session.Settings with { CallHistoryFile = path });
|
||||
session.ReloadSupportFiles();
|
||||
Status(session.History.Count > 0
|
||||
? $"{session.History.Count} calls read from {Path.GetFileName(path)}"
|
||||
: $"{Path.GetFileName(path)} holds no calls we could read");
|
||||
}
|
||||
|
||||
private async void OnDownloadCountryFile(object? sender, RoutedEventArgs e) =>
|
||||
await Download(
|
||||
downloader => downloader.DownloadCountryFileAsync(session.Paths.CountryFile),
|
||||
@@ -343,7 +364,7 @@ public sealed partial class EntryWindow
|
||||
{
|
||||
session.ReloadSupportFiles();
|
||||
Status($"country file {(session.Countries is null ? "missing" : "loaded")}, " +
|
||||
$"{session.Calls.Count} callsigns");
|
||||
$"{session.Calls.Count} callsigns, {session.History.Count} in the call history");
|
||||
}
|
||||
|
||||
private async Task Download(Func<SupportFileDownloader, Task<string>> fetch, string what)
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
<MenuItem Header="_Network…" Click="OnNetworkSettings" />
|
||||
<MenuItem Header="_Keyer and messages…" Click="OnKeyerSettings" />
|
||||
<Separator />
|
||||
<MenuItem Header="Call _History File…" Click="OnCallHistoryFile" />
|
||||
<Separator />
|
||||
<MenuItem Header="Download Country _File" Click="OnDownloadCountryFile" />
|
||||
<MenuItem Header="Download Check _Partial File" Click="OnDownloadCallDatabase" />
|
||||
<MenuItem Header="Reload Support Files" Click="OnReloadSupportFiles" />
|
||||
|
||||
@@ -315,6 +315,12 @@ public sealed partial class EntryWindow : Window
|
||||
}
|
||||
if (forward)
|
||||
{
|
||||
// leaving the callsign box is the moment the call is settled, which
|
||||
// is when the call history can say what the exchange will be
|
||||
if (Logging.Entry.Focus == 0 && Logging.FillFromHistory())
|
||||
{
|
||||
SyncBoxes();
|
||||
}
|
||||
Logging.Entry.Advance();
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user