Add N1MM's Mark and Store buttons
Both put something on our own bandmap and nothing on the cluster, which is what separates them from Spot It. Mark, Alt+M, leaves a mark where the radio is to say the frequency is busy. The label is N1MM's, Busy@ and the time. It is not a station, so Spot carries IsStation: nothing judges it, the check window does not offer it as a call, available mults and Qs leaves it out, space does not take it into the callsign box, and the bandmap paints it like a station already worked, which is how N1MM marks it. The station count under the bandmap counts stations. Store, Alt+O, puts the call being typed on the bandmap at this frequency so it can be worked later. N1MM writes "Local spot" in the comment and this does the same. The action row now holds the eight buttons N1MM has, in N1MM's order and at N1MM's widths: 23 per cent to Esc: Stop and 11 to each of the rest. Left out of Store: N1MM also adds the call to what the check window's Master column offers for the rest of the session. The call is on the bandmap, which the check window already reads, so it is offered either way. Driven under Xvfb: Alt+M leaving BUSY@22:32:27 on 14000, Alt+O storing SP9XYZ there, and both drawn on the bandmap in the worked colour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
20
README.md
20
README.md
@@ -247,13 +247,21 @@ them, between the entry boxes and the bearing line, which is where N1MM puts
|
|||||||
them. Each row fills the width, and the first button of the action row takes the
|
them. Each row fills the width, and the first button of the action row takes the
|
||||||
23 per cent N1MM gives it.
|
23 per cent N1MM gives it.
|
||||||
|
|
||||||
The action row is N1MM's: Esc: Stop, Wipe, Log It, Edit, Spot It and QRZ, each
|
The action row is N1MM's: Esc: Stop, Wipe, Log It, Edit, Mark, Store, Spot It
|
||||||
doing what its key does. QRZ opens the callsign's page in a browser.
|
and QRZ, each doing what its key does. QRZ opens the callsign's page in a
|
||||||
|
browser.
|
||||||
|
|
||||||
N1MM's other two buttons are not there. Both put something on your own bandmap
|
Mark and Store put something on your own bandmap rather than on the cluster, and
|
||||||
rather than on the cluster: Mark leaves a `Busy@` marker at the frequency you
|
have N1MM's keys:
|
||||||
are on, which needs a bandmap entry that is not a station, and Store puts the
|
|
||||||
call being typed there so you can come back to it.
|
- **Mark**, Alt+M, leaves a `Busy@` mark where the radio is, so you know not to
|
||||||
|
come back to that frequency. It is not a station: nothing judges it, it is
|
||||||
|
never offered as a call to work, space does not take it into the callsign box,
|
||||||
|
and the bandmap paints it like a station already worked, which is how N1MM
|
||||||
|
marks it.
|
||||||
|
- **Store**, Alt+O, puts the call being typed on the bandmap at this frequency
|
||||||
|
so it can be worked later. Spot It sends the same call to the cluster; Store
|
||||||
|
keeps it here.
|
||||||
|
|
||||||
The title bar says what N1MM's says: the frequency, the mode, whether the
|
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
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ public sealed partial class BandmapWindow : RefreshableWindow
|
|||||||
View.Vfos = Vfos();
|
View.Vfos = Vfos();
|
||||||
View.Segments = session.BandPlan.For(band);
|
View.Segments = session.BandPlan.For(band);
|
||||||
StatusText.Text =
|
StatusText.Text =
|
||||||
$"{View.Spots.Count} stations · {low.Kilohertz:0.#} to {high.Kilohertz:0.#} kHz{Split(band)}";
|
$"{View.Spots.Count(s => s.IsStation)} stations · " +
|
||||||
|
$"{low.Kilohertz:0.#} to {high.Kilohertz:0.#} kHz{Split(band)}";
|
||||||
View.InvalidateVisual();
|
View.InvalidateVisual();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +137,11 @@ public sealed partial class BandmapWindow : RefreshableWindow
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (!spot.IsStation)
|
||||||
|
{
|
||||||
|
// N1MM marks a busy frequency as worked, so it is painted like one
|
||||||
|
return Verdict.Dupe;
|
||||||
|
}
|
||||||
return session.Position.Log.Judge(new Qso
|
return session.Position.Log.Judge(new Qso
|
||||||
{
|
{
|
||||||
Id = "",
|
Id = "",
|
||||||
|
|||||||
@@ -47,11 +47,15 @@ public sealed partial class EntryWindow
|
|||||||
CallFrameText.Text = "";
|
CallFrameText.Text = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
framedCall = spot.Call.Text;
|
// a mark saying the frequency is busy is shown but never taken into the
|
||||||
|
// callsign box, because it is not a station
|
||||||
|
framedCall = spot.IsStation ? spot.Call.Text : "";
|
||||||
CallFrameText.Text = spot.IsSplit
|
CallFrameText.Text = spot.IsSplit
|
||||||
? $"{spot.Call.Text} — listening on {spot.Qsx.Kilohertz:0.0}"
|
? $"{spot.Call.Text} — listening on {spot.Qsx.Kilohertz:0.0}"
|
||||||
: spot.Call.Text;
|
: spot.Call.Text;
|
||||||
CallFrameText.Foreground = Verdicts.Colour(VerdictFor(spot));
|
CallFrameText.Foreground = spot.IsStation
|
||||||
|
? Verdicts.Colour(VerdictFor(spot))
|
||||||
|
: Verdicts.Dupe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Verdict? VerdictFor(Spot spot) => Logging?.Log.Judge(new Qso
|
private Verdict? VerdictFor(Spot spot) => Logging?.Log.Judge(new Qso
|
||||||
|
|||||||
@@ -351,6 +351,50 @@ public sealed partial class EntryWindow
|
|||||||
Status($"{call.Text} spotted on {where.Kilohertz:0.0}");
|
Status($"{call.Text} spotted on {where.Kilohertz:0.0}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// N1MM's Mark button and Alt+M: leaves a mark on our own bandmap saying
|
||||||
|
/// this frequency is busy, so the operator does not come back to it. It
|
||||||
|
/// goes nowhere near the cluster.
|
||||||
|
private void OnMark(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (Logging is null)
|
||||||
|
{
|
||||||
|
Status("no contest is open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Spot mark = Spot.Mark(Logging.Frequency, DateTime.UtcNow, session.Settings.Station.Callsign);
|
||||||
|
session.Bandmap.Add(mark);
|
||||||
|
Status($"{mark.Call.Text} on {mark.Frequency.Kilohertz:0.0}");
|
||||||
|
boxes[0].Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// N1MM's Store button and Alt+O: puts the call being typed on our own
|
||||||
|
/// bandmap at this frequency so it can be worked later. Spot It sends the
|
||||||
|
/// same call to the cluster; this one stays here.
|
||||||
|
private void OnStore(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (Logging is null)
|
||||||
|
{
|
||||||
|
Status("no contest is open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string typed = Logging.Entry.Call.Trim();
|
||||||
|
if (typed.Length == 0)
|
||||||
|
{
|
||||||
|
Status("nothing to store");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Callsign call = Callsign.Parse(typed);
|
||||||
|
session.Bandmap.Add(new Spot(
|
||||||
|
call,
|
||||||
|
Logging.Frequency,
|
||||||
|
DateTime.UtcNow,
|
||||||
|
SpotSource.Operator,
|
||||||
|
session.Settings.Station.Callsign,
|
||||||
|
"Local spot"));
|
||||||
|
Status($"{call.Text} stored on {Logging.Frequency.Kilohertz:0.0}");
|
||||||
|
boxes[0].Focus();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnShowLog(object? sender, RoutedEventArgs e) => Show(() => new LogWindow(session));
|
private void OnShowLog(object? sender, RoutedEventArgs e) => Show(() => new LogWindow(session));
|
||||||
|
|
||||||
private void OnShowCallStack(object? sender, RoutedEventArgs e) =>
|
private void OnShowCallStack(object? sender, RoutedEventArgs e) =>
|
||||||
|
|||||||
@@ -164,15 +164,17 @@
|
|||||||
<Grid Name="FunctionKeys" Margin="0,6,0,0"
|
<Grid Name="FunctionKeys" Margin="0,6,0,0"
|
||||||
ColumnDefinitions="*,*,*,*,*,*" RowDefinitions="Auto,Auto" />
|
ColumnDefinitions="*,*,*,*,*,*" RowDefinitions="Auto,Auto" />
|
||||||
|
|
||||||
<!-- the widths N1MM gives the row: 23 per cent to the first and the rest
|
<!-- the widths N1MM gives the row: 23 per cent to the first button and
|
||||||
shared out -->
|
11 to each of the other seven -->
|
||||||
<Grid ColumnDefinitions="115*,77*,77*,77*,77*,77*">
|
<Grid ColumnDefinitions="23*,11*,11*,11*,11*,11*,11*,11*">
|
||||||
<Button Classes="fkey" Content="Esc: Stop" Click="OnStopSending" />
|
<Button Classes="fkey" Content="Esc: Stop" Click="OnStopSending" />
|
||||||
<Button Grid.Column="1" Classes="fkey" Content="Wipe" Click="OnWipe" />
|
<Button Grid.Column="1" Classes="fkey" Content="Wipe" Click="OnWipe" />
|
||||||
<Button Grid.Column="2" Classes="fkey" Content="Log It" Click="OnLogIt" />
|
<Button Grid.Column="2" Classes="fkey" Content="Log It" Click="OnLogIt" />
|
||||||
<Button Grid.Column="3" Classes="fkey" Content="Edit" Click="OnEditLastContact" />
|
<Button Grid.Column="3" Classes="fkey" Content="Edit" Click="OnEditLastContact" />
|
||||||
<Button Grid.Column="4" Classes="fkey" Content="Spot It" Click="OnSpotIt" />
|
<Button Grid.Column="4" Classes="fkey" Content="Mark" Click="OnMark" />
|
||||||
<Button Grid.Column="5" Classes="fkey" Content="QRZ" Click="OnLookUpCall" />
|
<Button Grid.Column="5" Classes="fkey" Content="Store" Click="OnStore" />
|
||||||
|
<Button Grid.Column="6" Classes="fkey" Content="Spot It" Click="OnSpotIt" />
|
||||||
|
<Button Grid.Column="7" Classes="fkey" Content="QRZ" Click="OnLookUpCall" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<TextBlock Name="PathText" FontSize="11" Opacity="0.75" Margin="2,8,0,0" Text=""
|
<TextBlock Name="PathText" FontSize="11" Opacity="0.75" Margin="2,8,0,0" Text=""
|
||||||
|
|||||||
@@ -431,6 +431,14 @@ public sealed partial class EntryWindow : Window
|
|||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
DropStacked();
|
DropStacked();
|
||||||
break;
|
break;
|
||||||
|
case Key.M when e.KeyModifiers.HasFlag(KeyModifiers.Alt):
|
||||||
|
e.Handled = true;
|
||||||
|
OnMark(this, new RoutedEventArgs());
|
||||||
|
break;
|
||||||
|
case Key.O when e.KeyModifiers.HasFlag(KeyModifiers.Alt):
|
||||||
|
e.Handled = true;
|
||||||
|
OnStore(this, new RoutedEventArgs());
|
||||||
|
break;
|
||||||
case Key.OemQuestion when e.KeyModifiers.HasFlag(KeyModifiers.Control):
|
case Key.OemQuestion when e.KeyModifiers.HasFlag(KeyModifiers.Control):
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
ToggleRun();
|
ToggleRun();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public sealed class AvailableStations
|
|||||||
List<AvailableStation> found = [];
|
List<AvailableStation> found = [];
|
||||||
foreach (Spot spot in bandmap.All())
|
foreach (Spot spot in bandmap.All())
|
||||||
{
|
{
|
||||||
if (spot.Band is not { } band)
|
if (spot.Band is not { } band || !spot.IsStation)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,5 +99,9 @@ public sealed class CheckWindowSources
|
|||||||
private IReadOnlyList<string> HistoryCalls() => [.. session.Session.History.Calls];
|
private IReadOnlyList<string> HistoryCalls() => [.. session.Session.History.Calls];
|
||||||
|
|
||||||
private IReadOnlyList<string> SpottedCalls() =>
|
private IReadOnlyList<string> SpottedCalls() =>
|
||||||
bandmap.All().Select(s => s.Call.Text).Distinct(StringComparer.Ordinal).ToList();
|
bandmap.All()
|
||||||
|
.Where(s => s.IsStation)
|
||||||
|
.Select(s => s.Call.Text)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,26 @@ public sealed record Spot(
|
|||||||
/// comment. Zero when it is not.
|
/// comment. Zero when it is not.
|
||||||
public Frequency Qsx { get; init; }
|
public Frequency Qsx { get; init; }
|
||||||
|
|
||||||
|
/// False for a mark on the bandmap that is not a station. Nothing judges
|
||||||
|
/// one, nothing offers it as a call to work, and space does not take it
|
||||||
|
/// into the callsign box.
|
||||||
|
public bool IsStation { get; init; } = true;
|
||||||
|
|
||||||
|
/// N1MM's Mark button leaves one of these where the radio is, to say the
|
||||||
|
/// frequency is busy and not to come back to it. The label is N1MM's, and
|
||||||
|
/// carries the time the mark was left.
|
||||||
|
public static Spot Mark(Frequency where, DateTime atUtc, string spotter = "") =>
|
||||||
|
new(
|
||||||
|
Callsign.Parse($"Busy@{atUtc:HH:mm:ss}"),
|
||||||
|
where,
|
||||||
|
atUtc,
|
||||||
|
SpotSource.Operator,
|
||||||
|
spotter,
|
||||||
|
"Non-Counting Station")
|
||||||
|
{
|
||||||
|
IsStation = false,
|
||||||
|
};
|
||||||
|
|
||||||
public bool IsSplit => Qsx.Hertz > 0;
|
public bool IsSplit => Qsx.Hertz > 0;
|
||||||
|
|
||||||
public Band? Band => Bands.ForFrequency(Frequency);
|
public Band? Band => Bands.ForFrequency(Frequency);
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ public class AvailableStationsTests
|
|||||||
position.LogContact();
|
position.LogContact();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A mark saying a frequency is busy is not a station to work.
|
||||||
|
[Fact]
|
||||||
|
public void AMarkIsNotOffered()
|
||||||
|
{
|
||||||
|
(_, Bandmap bandmap, AvailableStations available) = Station();
|
||||||
|
bandmap.Add(Spot.Mark(On20, DateTime.UtcNow));
|
||||||
|
|
||||||
|
Assert.Empty(available.All());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ASpottedStationThatIsANewCountryIsAMultiplier()
|
public void ASpottedStationThatIsANewCountryIsAMultiplier()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -79,6 +79,19 @@ public class BandmapTests
|
|||||||
Assert.Single(map.All());
|
Assert.Single(map.All());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// N1MM's Mark button leaves this where the radio is, to say the frequency
|
||||||
|
/// is busy.
|
||||||
|
[Fact]
|
||||||
|
public void AMarkIsLabelledWithTheTimeAndIsNotAStation()
|
||||||
|
{
|
||||||
|
Spot mark = Spot.Mark(
|
||||||
|
Frequency.FromKilohertz(14_030),
|
||||||
|
new DateTime(2026, 1, 1, 22, 31, 5, DateTimeKind.Utc));
|
||||||
|
|
||||||
|
Assert.Equal("BUSY@22:31:05", mark.Call.Text);
|
||||||
|
Assert.False(mark.IsStation);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TheNearestSpotInsideTheWindowIsFound()
|
public void TheNearestSpotInsideTheWindowIsFound()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user