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(); } }