The window shows the operator, the call-frame spot, country, bearing and sun lines, the WWV line and a message area. It draws both rate graphs against the goals and shows the QSO timer (seven modes), the band timer, the band-change count and the Run/Mult box. Goals use N1MM's `dhh goal` file format and its Import Goals rules, including statistics and CSV files. Logic that does not need the UI is in Nonemm.Contests (Goals, QsoTimer, RateBars) and Nonemm.Session (InfoLines), with tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
189 lines
6.3 KiB
C#
189 lines
6.3 KiB
C#
using Avalonia.Controls;
|
|
using Avalonia.Controls.Documents;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
using Nonemm.Contests;
|
|
using Nonemm.Core;
|
|
using Nonemm.Core.Country;
|
|
using Nonemm.Session;
|
|
using Nonemm.Spotting;
|
|
|
|
namespace Nonemm.App.Windows;
|
|
|
|
/// N1MM's info window: who is operating, what is known about the station in the
|
|
/// call box, the rate graphs against the goals, and the timers contests with a
|
|
/// rest rule or a band-change rule ask the operator to watch.
|
|
public sealed partial class InfoWindow : RefreshableWindow
|
|
{
|
|
/// How far from the radio's frequency a spot may be and still be the one
|
|
/// the call-frame line shows.
|
|
private static readonly Frequency SpotReach = Frequency.FromHertz(500);
|
|
|
|
private readonly AppSession session;
|
|
private readonly DispatcherTimer clock;
|
|
private readonly Throttled later;
|
|
private readonly EventHandler onNotices;
|
|
private readonly EventHandler onSolar;
|
|
private int ticks;
|
|
|
|
public InfoWindow(AppSession session)
|
|
{
|
|
this.session = session;
|
|
InitializeComponent();
|
|
later = new Throttled(TimeSpan.FromSeconds(2), Refresh, holdWhileTyping: true);
|
|
clock = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
|
clock.Tick += (_, _) =>
|
|
{
|
|
ShowTimers();
|
|
if (++ticks % GraphEveryTicks == 0)
|
|
{
|
|
ShowGraphs();
|
|
}
|
|
};
|
|
clock.Start();
|
|
onNotices = (_, _) => Dispatcher.UIThread.Post(ShowMessages);
|
|
onSolar = (_, _) => Dispatcher.UIThread.Post(ShowWwv);
|
|
session.NoticesChanged += onNotices;
|
|
session.SolarChanged += onSolar;
|
|
Closed += (_, _) =>
|
|
{
|
|
clock.Stop();
|
|
later.Cancel();
|
|
session.NoticesChanged -= onNotices;
|
|
session.SolarChanged -= onSolar;
|
|
};
|
|
Refresh();
|
|
}
|
|
|
|
private ContestSession? Logging => session.Logging;
|
|
|
|
private Configuration.StoredInfoWindow Info => session.Settings.Info;
|
|
|
|
public override void RefreshFromSession() => later.Request();
|
|
|
|
public override void RefreshForEntry() => ShowLines();
|
|
|
|
public override void Refresh()
|
|
{
|
|
ShowHeader();
|
|
ShowLines();
|
|
ShowWwv();
|
|
ShowMessages();
|
|
ShowGraphs();
|
|
ShowTimers();
|
|
}
|
|
|
|
/// N1MM's caption: `Info - DL1ABC - Exch: 599 001`, where the number is the
|
|
/// next one to be sent in a contest that sends one.
|
|
private void ShowHeader()
|
|
{
|
|
string station = session.Settings.Station.Callsign;
|
|
if (Logging is null || session.Position is not { } position)
|
|
{
|
|
Title = $"Info - {station}";
|
|
OperatorText.Text = station;
|
|
StatusText.Text = "";
|
|
return;
|
|
}
|
|
string report = position.Entry.Exchange.Any(f => f.Slot == Contests.ExchangeSlot.SentReport)
|
|
? position.DefaultReport()
|
|
: "";
|
|
string sent = Logging.Instance.SentExchange;
|
|
string exchange = sent.Contains("001") || sent.Contains('#')
|
|
? position.SentNumber.ToString().PadLeft(3, '0')
|
|
: sent;
|
|
Title = $"Info - {station} - Exch: {report} {exchange}";
|
|
OperatorText.Text = Logging.Operator.Length > 0
|
|
? Logging.Operator
|
|
: session.Settings.CurrentOperator.Length > 0 ? session.Settings.CurrentOperator : station;
|
|
StatusText.Text = Info.ShowGoals ? $"Contest Start Time: {Logging.Instance.StartDate:yyyy-MM-dd HH:mm:ss}Z" : "";
|
|
}
|
|
|
|
/// The spot, the country with its bearing, and the sun there. They follow
|
|
/// the call in the entry window, and the spot under the radio's frequency
|
|
/// when no call is being typed.
|
|
private void ShowLines()
|
|
{
|
|
SpotText.IsVisible = Info.CallFrameSpot;
|
|
CountryText.IsVisible = Info.CountryInfo;
|
|
SunText.IsVisible = Info.SunTimes;
|
|
SpotText.Text = "";
|
|
CountryText.Text = "";
|
|
SunText.Text = "";
|
|
if (session.Position is not { } position)
|
|
{
|
|
return;
|
|
}
|
|
DateTime now = DateTime.UtcNow;
|
|
string call = position.Entry.Call.Trim();
|
|
Spot? spot = SpotFor(position, call);
|
|
if (spot is not null)
|
|
{
|
|
SpotText.Text = InfoLines.Spot(spot, now);
|
|
}
|
|
CountryLookup? country = call.Length >= 2
|
|
? position.Country()
|
|
: spot is null ? null : session.Countries?.Find(spot.Call);
|
|
if (country is null)
|
|
{
|
|
return;
|
|
}
|
|
StationPath? path = call.Length >= 2
|
|
? StationPath.For(position, now)
|
|
: StationPath.ToCall(position, spot!.Call, now);
|
|
string where = InfoLines.Country(country, Logging?.Contest.UsesItuZones ?? false);
|
|
CountryText.Text = path is null ? where : $"{where}, {InfoLines.Bearing(path, position.Me.Continent == "NA")}";
|
|
SunLine sun = InfoLines.Sun(country, now);
|
|
SunText.Text = sun.Text;
|
|
if (sun.IsSoon)
|
|
{
|
|
SunText.Foreground = Brushes.OrangeRed;
|
|
}
|
|
else
|
|
{
|
|
SunText.ClearValue(TextBlock.ForegroundProperty);
|
|
}
|
|
}
|
|
|
|
private Spot? SpotFor(OperatingPosition position, string call)
|
|
{
|
|
if (call.Length < 2)
|
|
{
|
|
return session.Bandmap.Near(position.Frequency, SpotReach) is { IsStation: true } near ? near : null;
|
|
}
|
|
Band? band = position.Band;
|
|
return session.Bandmap.All()
|
|
.Where(s => s.IsStation && string.Equals(s.Call.Text, call, StringComparison.OrdinalIgnoreCase))
|
|
.OrderByDescending(s => s.Band == band)
|
|
.ThenByDescending(s => s.AtUtc)
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
private void ShowWwv()
|
|
{
|
|
WwvText.IsVisible = Info.Wwv;
|
|
WwvText.Text = session.Solar?.Original ?? "";
|
|
}
|
|
|
|
private void ShowMessages()
|
|
{
|
|
MessagesScroll.IsVisible = Info.Messages;
|
|
if (!Info.Messages)
|
|
{
|
|
return;
|
|
}
|
|
MessagesText.Inlines?.Clear();
|
|
foreach (Notice notice in session.Notices)
|
|
{
|
|
Run line = new($"{notice.AtUtc:HH:mm:ss} {notice.Text}\n");
|
|
if (notice.IsAlert)
|
|
{
|
|
line.FontWeight = FontWeight.Bold;
|
|
line.Foreground = Brushes.OrangeRed;
|
|
}
|
|
MessagesText.Inlines?.Add(line);
|
|
}
|
|
MessagesScroll.ScrollToEnd();
|
|
}
|
|
}
|