Lay the band panel out the way N1MM lays it out, and colour it
N1MM's band panel is a row per band and a column per mode the contest runs, with every cell painted by what the callsign being typed would be worth there: red for one new multiplier, green for more than one, blue for points, grey for a station already worked on that band and mode. With nothing typed every cell is grey. That is the panel's point — it says where a station is still needed without working the bands one at a time — and this had only the frame. BandPanel holds the rule and is unit-tested without a window. The bands are the six contest bands, with 30, 17 and 12 metres added for general logging: in N1MM that is the one entry of the hundred and thirty-five that shows the WARC bands, so Contest.ShowsWarcBands says so and GeneralLogging overrides it. The columns are the modes the contest runs, so CQ WW CW has one column and IARU has two, rather than always showing CW and phone. A digital entry gets one DIG column where N1MM has RTTY and PSK: dupes and points here go by mode category, and those two are the same category. The labels are N1MM's: the metres alone up to 10, then the unit from 6m up. The band the radio is on is ringed rather than filled, as N1MM rings it, so the ring does not hide the colour underneath. The cells are labels with a click handler rather than buttons, so the theme cannot paint over the colour under the pointer. Text on a filled cell is black or white by N1MM's rule: light is the three channels adding up to more than 384. Looked at under Xvfb in CQ WW CW: one CW column, six bands, grey with nothing typed, and with a JA typed red on every band but 20, where the zone and country are already claimed, and which carries the ring. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
19
README.md
19
README.md
@@ -214,12 +214,21 @@ The title bar says what N1MM's says: the frequency, the mode, whether the
|
|||||||
frequency came from a radio or was typed, and which radio the window belongs to
|
frequency came from a radio or was typed, and which radio the window belongs to
|
||||||
when there are two.
|
when there are two.
|
||||||
|
|
||||||
### The band buttons
|
### The band panel
|
||||||
|
|
||||||
Down the left of the entry window are the bands, CW in one column and phone in
|
Down the left of the entry window is N1MM's band panel: a row per band, a
|
||||||
the other, as N1MM has them. A button puts the radio where you left that band
|
column per mode the contest runs, and every cell painted with what the callsign
|
||||||
and mode, or at the start of that part of the band the first time. The button
|
being typed would be worth on that band in that mode — red for one new
|
||||||
for where the radio is now is marked.
|
multiplier, green for more than one, blue for points, grey for a station already
|
||||||
|
worked there. So the panel says where the station is still needed without
|
||||||
|
working the bands one at a time. With nothing typed every cell is grey, as N1MM
|
||||||
|
leaves it.
|
||||||
|
|
||||||
|
The bands are the six contest bands, with 30, 17 and 12 metres added for general
|
||||||
|
logging, which is the only entry N1MM shows them for. Clicking a cell puts the
|
||||||
|
radio where you left that band and mode, or at the start of that part of the
|
||||||
|
band the first time. The band and mode the radio is on is ringed rather than
|
||||||
|
filled, so the ring does not hide what the cell is saying.
|
||||||
|
|
||||||
### The call frame
|
### The call frame
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ public static class Themes
|
|||||||
Changed?.Invoke(null, EventArgs.Empty);
|
Changed?.Invoke(null, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Black on a light colour and white on a dark one, which is how N1MM picks
|
||||||
|
/// the text for a cell it has filled: light is the three channels adding up
|
||||||
|
/// to more than 384.
|
||||||
|
public static IBrush TextOn(Color background) =>
|
||||||
|
background.R + background.G + background.B > 384
|
||||||
|
? Brush(Colors.Black)
|
||||||
|
: Brush(Colors.White);
|
||||||
|
|
||||||
/// One brush per colour, because a brush is created for every row of every
|
/// One brush per colour, because a brush is created for every row of every
|
||||||
/// window that is painted.
|
/// window that is painted.
|
||||||
public static IBrush Brush(Color colour)
|
public static IBrush Brush(Color colour)
|
||||||
|
|||||||
@@ -32,13 +32,17 @@ public static class Verdicts
|
|||||||
: Worth;
|
: Worth;
|
||||||
|
|
||||||
/// The same four states as backgrounds, which is what N1MM paints behind a
|
/// The same four states as backgrounds, which is what N1MM paints behind a
|
||||||
/// spot.
|
/// spot and in the band panel.
|
||||||
public static IBrush Background(Verdict? verdict) =>
|
public static IBrush Background(Verdict? verdict) => Themes.Brush(BackgroundColour(verdict));
|
||||||
verdict is null ? Themes.Brush(Themes.Current.WorthBackground)
|
|
||||||
: verdict.IsDupe ? Themes.Brush(Themes.Current.DupeBackground)
|
/// The same as a colour, for a caller that has to pick readable text to go
|
||||||
: verdict.NewMultipliers.Count > 1 ? Themes.Brush(Themes.Current.MultipliersBackground)
|
/// on top of it.
|
||||||
: verdict.NewMultipliers.Count == 1 ? Themes.Brush(Themes.Current.MultiplierBackground)
|
public static Color BackgroundColour(Verdict? verdict) =>
|
||||||
: Themes.Brush(Themes.Current.WorthBackground);
|
verdict is null ? Themes.Current.WorthBackground
|
||||||
|
: verdict.IsDupe ? Themes.Current.DupeBackground
|
||||||
|
: verdict.NewMultipliers.Count > 1 ? Themes.Current.MultipliersBackground
|
||||||
|
: verdict.NewMultipliers.Count == 1 ? Themes.Current.MultiplierBackground
|
||||||
|
: Themes.Current.WorthBackground;
|
||||||
|
|
||||||
public static string Describe(Verdict? verdict)
|
public static string Describe(Verdict? verdict)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,62 +1,122 @@
|
|||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Nonemm.App.Theming;
|
||||||
|
using Nonemm.Contests;
|
||||||
using Nonemm.Core;
|
using Nonemm.Core;
|
||||||
|
using Nonemm.Session;
|
||||||
|
|
||||||
namespace Nonemm.App.Windows;
|
namespace Nonemm.App.Windows;
|
||||||
|
|
||||||
/// The band buttons down the left of the window, CW in one column and phone in
|
/// The band panel down the left of the window, laid out as N1MM lays it out: a
|
||||||
/// the other, as N1MM has them. A button puts the radio where the operator left
|
/// column per mode the contest runs, a row per band, and each cell painted with
|
||||||
/// that band, or at the start of that part of it.
|
/// what the callsign being typed would be worth there. Clicking a cell puts the
|
||||||
|
/// radio where the operator left that band, or at the start of that part of it.
|
||||||
public sealed partial class EntryWindow
|
public sealed partial class EntryWindow
|
||||||
{
|
{
|
||||||
private static readonly IReadOnlyList<Band> BandColumn =
|
/// From here up a band is named with its unit — 6m, 2m, 70cm — and below it
|
||||||
[
|
/// with the metres alone.
|
||||||
Bands.Band160M, Bands.Band80M, Bands.Band40M, Bands.Band30M, Bands.Band20M,
|
private const double VhfFromMegahertz = 50;
|
||||||
Bands.Band17M, Bands.Band15M, Bands.Band12M, Bands.Band10M, Bands.Band6M,
|
|
||||||
];
|
|
||||||
|
|
||||||
private readonly List<(Band Band, ModeCategory Mode, Button Button)> bandButtons = [];
|
private readonly List<BandCell> bandCells = [];
|
||||||
|
private string bandsBuiltFor = "";
|
||||||
|
|
||||||
private void BuildBandButtons()
|
private sealed record BandCell(Band Band, ModeCategory Mode, Border Fill, Border Ring, TextBlock Label);
|
||||||
|
|
||||||
|
private void BuildBandCells(BandPanel panel)
|
||||||
{
|
{
|
||||||
|
string wanted = string.Join(
|
||||||
|
'|',
|
||||||
|
[.. panel.Bands.Select(b => b.Name), .. panel.Modes.Select(m => m.ToString())]);
|
||||||
|
if (wanted == bandsBuiltFor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bandsBuiltFor = wanted;
|
||||||
BandButtons.Children.Clear();
|
BandButtons.Children.Clear();
|
||||||
BandButtons.RowDefinitions.Clear();
|
BandButtons.RowDefinitions.Clear();
|
||||||
bandButtons.Clear();
|
BandButtons.ColumnDefinitions.Clear();
|
||||||
|
bandCells.Clear();
|
||||||
|
foreach (ModeCategory _ in panel.Modes)
|
||||||
|
{
|
||||||
|
BandButtons.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
||||||
|
}
|
||||||
BandButtons.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
BandButtons.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||||
Heading("CW", 0);
|
for (int column = 0; column < panel.Modes.Count; column++)
|
||||||
Heading("PH", 1);
|
{
|
||||||
for (int at = 0; at < BandColumn.Count; at++)
|
AddHeading(HeadingFor(panel.Modes[column]), column);
|
||||||
|
}
|
||||||
|
for (int row = 0; row < panel.Bands.Count; row++)
|
||||||
{
|
{
|
||||||
BandButtons.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
BandButtons.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||||
Add(BandColumn[at], ModeCategory.Cw, at + 1, column: 0);
|
for (int column = 0; column < panel.Modes.Count; column++)
|
||||||
Add(BandColumn[at], ModeCategory.Phone, at + 1, column: 1);
|
{
|
||||||
|
AddCell(panel.Bands[row], panel.Modes[column], row + 1, column);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Heading(string text, int column)
|
private void AddHeading(string text, int column)
|
||||||
{
|
{
|
||||||
TextBlock heading = new()
|
TextBlock heading = new()
|
||||||
{
|
{
|
||||||
Text = text,
|
Text = text,
|
||||||
FontSize = 10,
|
FontSize = 10,
|
||||||
Opacity = 0.7,
|
FontWeight = FontWeight.Bold,
|
||||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
Margin = new Avalonia.Thickness(0, 0, 2, 2),
|
Margin = new Thickness(0, 0, 2, 2),
|
||||||
};
|
};
|
||||||
Grid.SetColumn(heading, column);
|
Grid.SetColumn(heading, column);
|
||||||
BandButtons.Children.Add(heading);
|
BandButtons.Children.Add(heading);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Add(Band band, ModeCategory mode, int row, int column)
|
private void AddCell(Band band, ModeCategory mode, int row, int column)
|
||||||
{
|
{
|
||||||
Button button = new() { Content = band.MegahertzLabel.ToString("0.###") };
|
TextBlock label = new()
|
||||||
button.Classes.Add("band");
|
{
|
||||||
button.Click += (_, _) => MoveToBand(band, mode);
|
Text = LabelFor(band),
|
||||||
Grid.SetRow(button, row);
|
FontSize = 11,
|
||||||
Grid.SetColumn(button, column);
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
BandButtons.Children.Add(button);
|
};
|
||||||
bandButtons.Add((band, mode, button));
|
Border ring = new()
|
||||||
|
{
|
||||||
|
CornerRadius = new CornerRadius(10),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
BorderBrush = Brushes.Transparent,
|
||||||
|
Padding = new Thickness(6, 1),
|
||||||
|
Margin = new Thickness(2),
|
||||||
|
Child = label,
|
||||||
|
};
|
||||||
|
Border fill = new()
|
||||||
|
{
|
||||||
|
Margin = new Thickness(0, 0, 2, 0),
|
||||||
|
MinWidth = 34,
|
||||||
|
Cursor = new Cursor(StandardCursorType.Hand),
|
||||||
|
Child = ring,
|
||||||
|
};
|
||||||
|
fill.PointerPressed += (_, _) => MoveToBand(band, mode);
|
||||||
|
Grid.SetRow(fill, row);
|
||||||
|
Grid.SetColumn(fill, column);
|
||||||
|
BandButtons.Children.Add(fill);
|
||||||
|
bandCells.Add(new BandCell(band, mode, fill, ring, label));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// N1MM names the HF bands with the metres alone and the VHF bands with the
|
||||||
|
/// unit: 160, 80, 40 … then 6m, 2m, 70cm.
|
||||||
|
private static string LabelFor(Band band) =>
|
||||||
|
band.MegahertzLabel < VhfFromMegahertz
|
||||||
|
? band.Name[..^1]
|
||||||
|
: band.Name.ToLowerInvariant();
|
||||||
|
|
||||||
|
private static string HeadingFor(ModeCategory mode) => mode switch
|
||||||
|
{
|
||||||
|
ModeCategory.Phone => "PH",
|
||||||
|
ModeCategory.Digital => "DIG",
|
||||||
|
_ => "CW",
|
||||||
|
};
|
||||||
|
|
||||||
private void MoveToBand(Band band, ModeCategory mode)
|
private void MoveToBand(Band band, ModeCategory mode)
|
||||||
{
|
{
|
||||||
if (Logging is null)
|
if (Logging is null)
|
||||||
@@ -64,15 +124,22 @@ public sealed partial class EntryWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Frequency where = session.BandMemory.Where(band, mode, session.BandPlan);
|
Frequency where = session.BandMemory.Where(band, mode, session.BandPlan);
|
||||||
Mode wanted = mode == ModeCategory.Phone ? Modes.ForSideband(where) : Modes.Cw;
|
Mode wanted = mode switch
|
||||||
|
{
|
||||||
|
ModeCategory.Phone => Modes.ForSideband(where),
|
||||||
|
ModeCategory.Digital => Modes.Rtty,
|
||||||
|
_ => Modes.Cw,
|
||||||
|
};
|
||||||
Logging.Tune(where, wanted);
|
Logging.Tune(where, wanted);
|
||||||
_ = session.Radio?.TuneAsync(where);
|
_ = session.Radio?.TuneAsync(where);
|
||||||
boxes[0].Focus();
|
boxes[0].Focus();
|
||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The button for the band and mode the radio is on is marked, and the
|
/// Each cell says what the call being typed would be worth on that band and
|
||||||
/// frequency is kept so the button goes back to it.
|
/// mode. With nothing typed N1MM paints every cell as a station already
|
||||||
|
/// worked. The band the radio is on is ringed rather than filled, so the
|
||||||
|
/// ring does not hide what the cell is saying.
|
||||||
private void ShowBandButtons()
|
private void ShowBandButtons()
|
||||||
{
|
{
|
||||||
if (Logging is null)
|
if (Logging is null)
|
||||||
@@ -80,10 +147,21 @@ public sealed partial class EntryWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
session.BandMemory.Remember(Logging.Frequency, Logging.Mode);
|
session.BandMemory.Remember(Logging.Frequency, Logging.Mode);
|
||||||
|
BandPanel panel = new(Logging);
|
||||||
|
BuildBandCells(panel);
|
||||||
Band? here = Bands.ForFrequency(Logging.Frequency);
|
Band? here = Bands.ForFrequency(Logging.Frequency);
|
||||||
foreach ((Band band, ModeCategory mode, Button button) in bandButtons)
|
foreach (BandCell cell in bandCells)
|
||||||
{
|
{
|
||||||
button.Classes.Set("here", band == here && mode == Logging.Mode.Category);
|
Verdict? worth = panel.Worth(cell.Band, cell.Mode);
|
||||||
|
Color background = worth is null
|
||||||
|
? Themes.Current.DupeBackground
|
||||||
|
: Verdicts.BackgroundColour(worth);
|
||||||
|
IBrush text = Themes.TextOn(background);
|
||||||
|
cell.Fill.Background = Themes.Brush(background);
|
||||||
|
cell.Label.Foreground = text;
|
||||||
|
cell.Ring.BorderBrush = cell.Band == here && cell.Mode == Logging.Mode.Category
|
||||||
|
? text
|
||||||
|
: Brushes.Transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,20 +32,6 @@
|
|||||||
<Style Selector="Button.mode.here:pointerover /template/ ContentPresenter">
|
<Style Selector="Button.mode.here:pointerover /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="#3498DB" />
|
<Setter Property="Background" Value="#3498DB" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.band">
|
|
||||||
<Setter Property="FontSize" Value="11" />
|
|
||||||
<Setter Property="Padding" Value="4,1" />
|
|
||||||
<Setter Property="Margin" Value="0,0,2,2" />
|
|
||||||
<Setter Property="MinWidth" Value="34" />
|
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button.band.here">
|
|
||||||
<Setter Property="Background" Value="#2980B9" />
|
|
||||||
<Setter Property="Foreground" Value="White" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button.band.here:pointerover /template/ ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="#3498DB" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button.fkey">
|
<Style Selector="Button.fkey">
|
||||||
<Setter Property="FontSize" Value="11" />
|
<Setter Property="FontSize" Value="11" />
|
||||||
<Setter Property="Padding" Value="6,3" />
|
<Setter Property="Padding" Value="6,3" />
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ public sealed partial class EntryWindow : Window
|
|||||||
this.radioNumber = radioNumber;
|
this.radioNumber = radioNumber;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
BuildFunctionKeys();
|
BuildFunctionKeys();
|
||||||
BuildBandButtons();
|
|
||||||
session.Changed += (_, _) => Dispatcher.UIThread.Post(Refresh);
|
session.Changed += (_, _) => Dispatcher.UIThread.Post(Refresh);
|
||||||
session.ContestChanged += (_, _) => Dispatcher.UIThread.Post(() =>
|
session.ContestChanged += (_, _) => Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ public interface Contest
|
|||||||
/// The modes the contest runs; empty means any.
|
/// The modes the contest runs; empty means any.
|
||||||
IReadOnlyList<ModeCategory> Modes { get; }
|
IReadOnlyList<ModeCategory> Modes { get; }
|
||||||
|
|
||||||
|
/// Whether the band panel lists 30, 17 and 12 metres. Contests keep off the
|
||||||
|
/// WARC bands, so only general logging says yes, as in N1MM.
|
||||||
|
bool ShowsWarcBands => false;
|
||||||
|
|
||||||
string SentExchangeFor(StationInfo me);
|
string SentExchangeFor(StationInfo me);
|
||||||
|
|
||||||
int PointsFor(QsoContext qso);
|
int PointsFor(QsoContext qso);
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ public sealed class GeneralLogging : Contest
|
|||||||
|
|
||||||
public IReadOnlyList<ModeCategory> Modes => [];
|
public IReadOnlyList<ModeCategory> Modes => [];
|
||||||
|
|
||||||
|
public bool ShowsWarcBands => true;
|
||||||
|
|
||||||
public string SentExchangeFor(StationInfo me) => "";
|
public string SentExchangeFor(StationInfo me) => "";
|
||||||
|
|
||||||
public int PointsFor(QsoContext qso) => 0;
|
public int PointsFor(QsoContext qso) => 0;
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ public static class Bands
|
|||||||
public static readonly IReadOnlyList<Band> Contest =
|
public static readonly IReadOnlyList<Band> Contest =
|
||||||
[Band160M, Band80M, Band40M, Band20M, Band15M, Band10M];
|
[Band160M, Band80M, Band40M, Band20M, Band15M, Band10M];
|
||||||
|
|
||||||
|
/// The same, with the three WARC bands added. Contests keep off them, so
|
||||||
|
/// this is the list for general logging.
|
||||||
|
public static readonly IReadOnlyList<Band> ContestWithWarc =
|
||||||
|
[Band160M, Band80M, Band40M, Band30M, Band20M, Band17M, Band15M, Band12M, Band10M];
|
||||||
|
|
||||||
private static readonly Dictionary<string, Band> ByName =
|
private static readonly Dictionary<string, Band> ByName =
|
||||||
All.ToDictionary(b => b.Name, StringComparer.OrdinalIgnoreCase);
|
All.ToDictionary(b => b.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
|||||||
42
src/Nonemm.Session/BandPanel.cs
Normal file
42
src/Nonemm.Session/BandPanel.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Nonemm.Contests;
|
||||||
|
using Nonemm.Core;
|
||||||
|
|
||||||
|
namespace Nonemm.Session;
|
||||||
|
|
||||||
|
/// The band panel down the side of the entry window: a row per band, a column
|
||||||
|
/// per mode the contest runs, and what the callsign being typed would be worth
|
||||||
|
/// in each cell. N1MM paints the same panel, so an operator can see at a glance
|
||||||
|
/// where the station is still needed.
|
||||||
|
public sealed class BandPanel
|
||||||
|
{
|
||||||
|
private readonly OperatingPosition position;
|
||||||
|
|
||||||
|
public BandPanel(OperatingPosition position)
|
||||||
|
{
|
||||||
|
this.position = position;
|
||||||
|
Bands = position.Contest.ShowsWarcBands
|
||||||
|
? Core.Bands.ContestWithWarc
|
||||||
|
: Core.Bands.Contest;
|
||||||
|
Modes = position.Contest.Modes.Count > 0
|
||||||
|
? position.Contest.Modes
|
||||||
|
: [ModeCategory.Cw, ModeCategory.Phone];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<Band> Bands { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<ModeCategory> Modes { get; }
|
||||||
|
|
||||||
|
/// What working the call being typed on this band and mode would bring.
|
||||||
|
/// Null while there is no call to judge, which N1MM paints as a dupe.
|
||||||
|
public Verdict? Worth(Band band, ModeCategory mode) =>
|
||||||
|
position.Verdict(band.Low, ModeFor(mode));
|
||||||
|
|
||||||
|
/// Any mode of the category answers the same question: dupes are keyed on
|
||||||
|
/// the category, and no contest scores USB and LSB differently.
|
||||||
|
private static Mode ModeFor(ModeCategory category) => category switch
|
||||||
|
{
|
||||||
|
ModeCategory.Phone => Core.Modes.Usb,
|
||||||
|
ModeCategory.Digital => Core.Modes.Rtty,
|
||||||
|
_ => Core.Modes.Cw,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -72,6 +72,16 @@ public sealed class OperatingPosition
|
|||||||
return call.Length == 0 ? null : Log.Judge(BuildQso(call));
|
return call.Length == 0 ? null : Log.Judge(BuildQso(call));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The same on another band and mode, which is what the band panel asks for
|
||||||
|
/// each of its cells.
|
||||||
|
public Verdict? Verdict(Frequency frequency, Mode mode)
|
||||||
|
{
|
||||||
|
string call = Entry.Call.Trim();
|
||||||
|
return call.Length == 0
|
||||||
|
? null
|
||||||
|
: Log.Judge(BuildQso(call) with { Frequency = frequency, Mode = mode });
|
||||||
|
}
|
||||||
|
|
||||||
public CountryLookup? Country() =>
|
public CountryLookup? Country() =>
|
||||||
Entry.Call.Trim().Length == 0 ? null : Session.Countries?.Find(Entry.Call.Trim());
|
Entry.Call.Trim().Length == 0 ? null : Session.Countries?.Find(Entry.Call.Trim());
|
||||||
|
|
||||||
|
|||||||
117
tests/Nonemm.Session.Tests/BandPanelTests.cs
Normal file
117
tests/Nonemm.Session.Tests/BandPanelTests.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using Nonemm.Contests;
|
||||||
|
using Nonemm.Contests.Rules;
|
||||||
|
using Nonemm.Core;
|
||||||
|
using Nonemm.Core.Country;
|
||||||
|
using Nonemm.Storage;
|
||||||
|
|
||||||
|
namespace Nonemm.Session.Tests;
|
||||||
|
|
||||||
|
public class BandPanelTests
|
||||||
|
{
|
||||||
|
private const string Countries = """
|
||||||
|
Germany: 14: 28: EU: 51.00: -10.00: -1.0: DL:
|
||||||
|
DL,DK,DJ;
|
||||||
|
Japan: 25: 45: AS: 36.40: -138.38: -9.0: JA:
|
||||||
|
JA,JH,JR;
|
||||||
|
""";
|
||||||
|
|
||||||
|
private static readonly StationInfo Me = new()
|
||||||
|
{
|
||||||
|
Callsign = "DL1ABC",
|
||||||
|
CqZone = 14,
|
||||||
|
ItuZone = 28,
|
||||||
|
Continent = "EU",
|
||||||
|
CountryPrefix = "DL",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static (OperatingPosition Position, BandPanel Panel) Station(Contest? contest = null)
|
||||||
|
{
|
||||||
|
FakeLogStore store = new();
|
||||||
|
ContestInstance instance = store.AddContest(new ContestInstance
|
||||||
|
{
|
||||||
|
ContestNumber = 0,
|
||||||
|
ContestName = "CQWW",
|
||||||
|
});
|
||||||
|
ContestSession session = new(
|
||||||
|
store,
|
||||||
|
contest ?? new CqWorldWide(ModeCategory.Cw),
|
||||||
|
instance,
|
||||||
|
Me,
|
||||||
|
CountryFile.Parse(Countries),
|
||||||
|
null);
|
||||||
|
OperatingPosition position = new(session);
|
||||||
|
return (position, new BandPanel(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Work(OperatingPosition position, string call, string zone, Frequency where)
|
||||||
|
{
|
||||||
|
position.Tune(where, Modes.Cw);
|
||||||
|
position.Entry.Call = call;
|
||||||
|
position.Entry.Set(ExchangeSlot.ReceivedReport, "599");
|
||||||
|
position.Entry.Set(ExchangeSlot.Zone, zone);
|
||||||
|
position.LogContact();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AContestListsTheContestBands()
|
||||||
|
{
|
||||||
|
(_, BandPanel panel) = Station();
|
||||||
|
|
||||||
|
Assert.Equal(Bands.Contest, panel.Bands);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GeneralLoggingAddsTheWarcBands()
|
||||||
|
{
|
||||||
|
(_, BandPanel panel) = Station(new GeneralLogging());
|
||||||
|
|
||||||
|
Assert.Equal(Bands.ContestWithWarc, panel.Bands);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TheColumnsAreTheModesTheContestRuns()
|
||||||
|
{
|
||||||
|
(_, BandPanel cw) = Station();
|
||||||
|
(_, BandPanel both) = Station(new IaruHf());
|
||||||
|
|
||||||
|
Assert.Equal([ModeCategory.Cw], cw.Modes);
|
||||||
|
Assert.Equal([ModeCategory.Cw, ModeCategory.Phone], both.Modes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AContestThatNamesNoModeGetsCwAndPhone()
|
||||||
|
{
|
||||||
|
(_, BandPanel panel) = Station(new GeneralLogging());
|
||||||
|
|
||||||
|
Assert.Equal([ModeCategory.Cw, ModeCategory.Phone], panel.Modes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithNothingTypedThereIsNothingToJudge()
|
||||||
|
{
|
||||||
|
(_, BandPanel panel) = Station();
|
||||||
|
|
||||||
|
Assert.Null(panel.Worth(Bands.Band20M, ModeCategory.Cw));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ACallWorkedOnOneBandIsADupeThereAndWorthWorkingOnTheOthers()
|
||||||
|
{
|
||||||
|
(OperatingPosition position, BandPanel panel) = Station();
|
||||||
|
Work(position, "JA1XYZ", "25", Bands.Band20M.Low);
|
||||||
|
position.Entry.Call = "JA1XYZ";
|
||||||
|
|
||||||
|
Assert.True(panel.Worth(Bands.Band20M, ModeCategory.Cw)?.IsDupe);
|
||||||
|
Assert.False(panel.Worth(Bands.Band15M, ModeCategory.Cw)?.IsDupe);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ACallIsANewMultiplierOnEveryBandItHasNotBeenWorkedOn()
|
||||||
|
{
|
||||||
|
(OperatingPosition position, BandPanel panel) = Station();
|
||||||
|
position.Entry.Call = "JA1XYZ";
|
||||||
|
|
||||||
|
Assert.True(panel.Worth(Bands.Band20M, ModeCategory.Cw)?.IsNewMultiplier);
|
||||||
|
Assert.True(panel.Worth(Bands.Band40M, ModeCategory.Cw)?.IsNewMultiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user