diff --git a/README.md b/README.md index df5442c..d13deeb 100644 --- a/README.md +++ b/README.md @@ -663,11 +663,47 @@ rather than guessing. ### Available mults and Qs -**View → Available Mults and Qs** lists what is spotted and not worked yet, a -column per band, multipliers first and then the stations that are only points. -Clicking one puts the radio there with the call in the entry window, the same as -clicking a spot on the bandmap. **Mults only** hides everything that brings no -multiplier. +**View → Available Mults and Qs** lists what is spotted and not worked yet. The +top shows a table with the multipliers, the Qs and the total on each band. +Under it is one line per station. Click a header to sort on that column, and +shift-click a second header to add a second sort key. The line is coloured the +same as in the entry window, and the one the radio is on is drawn in reverse. + +| Column | Shows | +| --- | --- | +| Call, Freq, Mode | the spot; `»` after the frequency marks a split | +| Dir | beam heading from the grid in call history, or the country file | +| Mlt | how many multipliers the station would bring | +| SN | decibels from a Skimmer spot's comment | +| TS | spot time in UTC; `◴` when the spot is within 3 minutes of dropping off the bandmap | +| Spotter | `(#)` after a Skimmer or beacon spotter | +| Grid | grid square from call history | +| Sun | `▲` or `▼` and the minutes to (+) or since (-) sunrise or sunset at the station, within one hour | +| Pts | points the contact would bring | +| ★ | the spotter is in the telnet window's preferred spotters | + +Right-click a header to show or hide a column, or to resize the columns to their +contents. Drag a header to move a column. + +- Click a line to put the radio there with the call in the entry window. Click + with the middle button, or right-click ▸ **Delete Spot**, to remove the spot + from the bandmap. **Blacklist callsign** and **Blacklist spotter** add the + name to the telnet window's blocked lists and remove its spots. +- Alt+A picks the next line and tunes to it, Shift+Alt+A the previous one and + Ctrl+Alt+A the first. Nothing happens until a line is picked. +- While the mouse is over the list the list stops re-sorting for six seconds, so + the line being aimed at stays put. The window says "Scrolling Pausing". +- Click a band in the table to show only that band. Shift-click adds more bands. + Click a number in the Mults row to show that band's multipliers, or in the Qs + row to show its multipliers and Qs. Click Mults, Qs or Total at the left to + show every band. +- The **Mults** / **Mults & Qs** button shows what the list holds now, and a + click switches it. **Bands & Modes** limits the list to some bands and modes + without touching the telnet filters. Both are kept between runs. +- The right-click menu also holds **Summary Panel Band Selection**, **Show + Summary Panel** and **Help**. With the list sorted on TS, newest first, + **QSY to top spot after logging QSO** tunes to the newest spot after each + contact logged while searching and pouncing. A spot carries no exchange, so a station is only known to be a multiplier where the multiplier follows from the callsign — a DXCC entity — or where the call diff --git a/docs/unfinished.md b/docs/unfinished.md index b447a12..67095c2 100644 --- a/docs/unfinished.md +++ b/docs/unfinished.md @@ -789,7 +789,50 @@ those bands contradict themselves — its CW top for 1.25M is below its phone start, and its 70CM CW top is above the band. Config ▸ Sub bands cannot add a band that has no default. -**Available mults and Qs is only as good as the bandmap.** It lists what is +**Available Mults and Qs differs from N1MM's window.** Reference: +https://n1mmwp.hamdocs.com/manual-windows/available-mults-and-qs-window/ + +Needs SO2R, which has two radio positions and two entry windows already: + +- the window judges and tunes with the first radio's position only; +- Focus Radio, Non-Focus Radio and Only Radio 1/2 filters; +- Shift-click sends a spot to the other radio; +- Spot to Associated Radio, with the warning when the window opens; +- Focus Follows Spot, which moves focus to the radio the spot went to; +- a second Available window, one per radio; +- Enable Right-click QSY on Spot, which sends the spot to the non-focus radio; +- Alt+B, Shift+Alt+B and Ctrl+Alt+B for the second window. + +Needs SO2V, which is not implemented at all (the program has split transmit +through rigctld `S 1 VFOB` and a bandmap bar for the second radio, nothing else): + +- reading the sub-receiver or VFO B from the rig, and a position for it; +- Basic and Advanced SO2V VFO control, where a spot goes to the VFO already on + its band, otherwise the active VFO (Basic always uses VFO A); +- the Basic or Advanced label in the title bar. + +Needs something the program does not have: + +- the ♥ column, because there is no Special Calls list in the telnet window; +- the heard column, because there is no spectrum display; +- Turn Rotor, because there is no rotator control; +- the `(!)` marker for a local Skimmer, because a spot does not say whether the + Skimmer is ours. + +Smaller differences: + +- the TS header does not change colour while QSY after logging is on; +- the last picked line has the grid's own selection, not N1MM's light green + frame; +- a left click does both the QSY and the call load, where N1MM loads the call + only on a double click; +- the title bar shows the Qs, Mults and total counts, where N1MM also shows the + VFO or focus mode; +- there is no option to turn off the Pts column's calculation to save CPU; +- tooltips are always on, not tied to a setting; +- the column order is not kept between runs, only which columns are hidden. + +**Available Mults and Qs is only as good as the bandmap.** It lists what is spotted, so a multiplier nobody has spotted is not there. N1MM's window also shows the mults a contest has and nobody has worked, which needs a list of every multiplier the contest counts. Only the section and state lists are held here, diff --git a/src/Nonemm.App/Configuration/Settings.cs b/src/Nonemm.App/Configuration/Settings.cs index 52b2404..2899ea4 100644 --- a/src/Nonemm.App/Configuration/Settings.cs +++ b/src/Nonemm.App/Configuration/Settings.cs @@ -85,6 +85,28 @@ public sealed record Settings public int AvailableFontSize { get; init; } = 13; + /// The Available Mults and Qs window lists only the stations that bring a + /// multiplier. Off lists the stations that are only points as well. + public bool AvailableMultsOnly { get; init; } + + /// Headers of the columns the window hides. Call cannot be hidden. + public IReadOnlyList AvailableHiddenColumns { get; init; } = []; + + /// Band names the window is limited to. Empty is every band. + public IReadOnlyList AvailableBands { get; init; } = []; + + /// `CW`, `PHONE` or `DIGITAL`. Empty is every mode. + public IReadOnlyList AvailableModes { get; init; } = []; + + /// Band names in the summary table. Empty is the contest's bands. + public IReadOnlyList AvailableSummaryBands { get; init; } = []; + + public bool AvailableShowsSummary { get; init; } = true; + + /// After a contact is logged while searching and pounce, the radio goes to + /// the newest spot. Needs the list sorted on TS, newest first. + public bool AvailableQsyAfterLogging { get; init; } + /// Moves each incoming CW spot by a few tens of hertz, so the operator has /// to find the station by ear. N1MM calls it randomising. public bool RandomizeSpots { get; init; } diff --git a/src/Nonemm.App/Windows/AvailableRow.cs b/src/Nonemm.App/Windows/AvailableRow.cs new file mode 100644 index 0000000..a318e60 --- /dev/null +++ b/src/Nonemm.App/Windows/AvailableRow.cs @@ -0,0 +1,155 @@ +using System.Globalization; +using Avalonia.Media; +using Nonemm.Core; +using Nonemm.Session; + +namespace Nonemm.App.Windows; + +/// One line of the available window. The text properties are what the cells +/// show; the typed ones are what the columns sort on, so 9 comes before 10. +public sealed class AvailableRow +{ + /// A spot this close to dropping off the bandmap gets a clock beside its + /// time. + private static readonly TimeSpan ExpiringWithin = TimeSpan.FromMinutes(3); + + /// The sun marker shows from this long before sunrise or sunset until this + /// long after. + private static readonly TimeSpan SunWindow = TimeSpan.FromHours(1); + + private readonly DateTime nowUtc; + + public AvailableRow( + AvailableStation station, + string mode, + DateTime nowUtc, + TimeSpan? remaining, + bool isPreferred, + bool isTuned) + { + Station = station; + Mode = mode; + this.nowUtc = nowUtc; + Remaining = remaining; + IsPreferred = isPreferred; + IsTuned = isTuned; + SunMinutes = SunMinutesFor(station, nowUtc); + } + + public AvailableStation Station { get; } + + public TimeSpan? Remaining { get; } + + public bool IsPreferred { get; } + + /// The radio is on this spot, which the window paints in reverse. + public bool IsTuned { get; } + + public string Call => Station.Spot.Call.Text; + + public double Kilohertz => Station.Spot.Frequency.Kilohertz; + + public string Frequency => Kilohertz.ToString("0.0", CultureInfo.InvariantCulture) + (Station.Spot.IsSplit ? " »" : ""); + + public string Mode { get; } + + public int? Heading => Station.Heading is { } heading ? (int)Math.Round(heading) % 360 : null; + + public string Direction => Heading?.ToString(CultureInfo.InvariantCulture) ?? ""; + + public int Multipliers => Station.NewMultipliers.Count; + + public int? Snr => Station.Spot.Snr; + + public string Noise => Snr?.ToString(CultureInfo.InvariantCulture) ?? ""; + + public DateTime AtUtc => Station.Spot.AtUtc; + + public bool IsExpiring => Remaining is { } left && left <= ExpiringWithin; + + public string Time => AtUtc.ToString("HH:mm", CultureInfo.InvariantCulture) + (IsExpiring ? " ◴" : ""); + + public string Spotter => Station.Spot.Spotter + (Station.Spot.IsSkimmer ? " (#)" : ""); + + public string Grid => Station.Grid; + + /// Minutes to the next sunrise or sunset at the station, negative once it + /// has passed, or `int.MaxValue` when the sun is not within the hour. + public int SunMinutes { get; } + + public string Sun => SunMinutes == int.MaxValue ? "" : SunText; + + public int Points => Station.Verdict.Points; + + public string Star => IsPreferred ? "★" : ""; + + public IBrush Colour => Verdicts.Colour(Station.Verdict); + + public IBrush Background => Verdicts.Background(Station.Verdict); + + /// What a refresh compares to see whether the list changed. + public string Key => + $"{Call}|{Station.Band.Name}|{Frequency}|{Multipliers}|{Points}|{Spotter}|{Time}|{Sun}|{Star}|{IsTuned}"; + + public string Tip + { + get + { + List lines = [$"{Call} spotted by {Station.Spot.Spotter}", $"{(int)(nowUtc - AtUtc).TotalMinutes} min ago"]; + if (Station.Spot.Comment.Length > 0) + { + lines.Add(Station.Spot.Comment); + } + if (Station.Spot.IsSplit) + { + lines.Add($"listening on {Station.Spot.Qsx.Kilohertz:0.0}"); + } + if (Sun.Length > 0) + { + lines.Add(SunNote); + } + return string.Join('\n', lines); + } + } + + private string SunText + { + get + { + (bool isRise, int minutes) = Nearest(Station, nowUtc)!.Value; + return $"{(isRise ? "▲" : "▼")}{minutes:+0;-0;0}"; + } + } + + private string SunNote + { + get + { + (bool isRise, int minutes) = Nearest(Station, nowUtc)!.Value; + string when = minutes >= 0 ? $"in {minutes} min" : $"{-minutes} min ago"; + return $"{(isRise ? "sunrise" : "sunset")} at the station {when}"; + } + } + + private static int SunMinutesFor(AvailableStation station, DateTime nowUtc) => + Nearest(station, nowUtc) is { } near ? near.Minutes : int.MaxValue; + + /// The sunrise or sunset within the hour either side, whichever is nearer. + private static (bool IsRise, int Minutes)? Nearest(AvailableStation station, DateTime nowUtc) + { + (bool IsRise, int Minutes)? best = null; + foreach ((bool isRise, DateTime? at) in new[] { (true, station.Path?.Sunrise), (false, station.Path?.Sunset) }) + { + if (at is not { } time || (time - nowUtc).Duration() > SunWindow) + { + continue; + } + int minutes = (int)Math.Round((time - nowUtc).TotalMinutes); + if (best is null || Math.Abs(minutes) < Math.Abs(best.Value.Minutes)) + { + best = (isRise, minutes); + } + } + return best; + } +} diff --git a/src/Nonemm.App/Windows/AvailableWindow.axaml b/src/Nonemm.App/Windows/AvailableWindow.axaml index 387cf07..27a93b1 100644 --- a/src/Nonemm.App/Windows/AvailableWindow.axaml +++ b/src/Nonemm.App/Windows/AvailableWindow.axaml @@ -1,15 +1,37 @@ + Title="Available Mults and Qs" Width="780" Height="440"> - +