Move and batch spots the way N1MM does

Two differences from N1MM in the spot path, both found by reading Packet.cs.

Randomising moved every CW spot. PacketSpot.Randomize moves a spot only when it
is not a dupe, not a self spot, not on a CQ frequency, is on CW and is not
working split, and Packet skips it altogether for a station passed to another
band. Moving a dupe or a split station makes it harder to find, not easier, so
this now leaves the same ones alone. Whether a station has been worked comes
from the log, so AppSession asks it before moving a spot.

Split needed knowing where a station is listening, which is in the comment. Qsx
reads it by N1MM's rules: QSX, UP, DOWN, DN, U or D, the word standing alone or
after a space, a bare number after QSX meaning kilohertz inside the band and
anything else an offset from the spot, an offset over a hundred kilohertz
refused unless it says QSX, DN70 read as a grid square rather than five down,
and a listening frequency outside the band thrown away.

Spots reached the bandmap one at a time, each one redrawing every window that
watches it. N1MM queues them and flushes the queue once a second, which is what
this does now: the filters and the randomiser run over the batch, and the
bandmap takes it in one call and raises one change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 10:15:40 +00:00
parent f102e0a304
commit ac63987e0c
9 changed files with 267 additions and 19 deletions

View File

@@ -33,6 +33,13 @@ public sealed class AppSession : IDisposable
private readonly Random jitter = new();
/// Spots wait here until the next flush. N1MM collects them the same way,
/// because a skimmer feed sends more spots a minute than any window can
/// usefully redraw.
private readonly List<Spot> arriving = [];
private readonly Lock arrivingGuard = new();
private readonly Timer spotFlush;
public AppSession(UserPaths paths, Settings settings)
{
Paths = paths;
@@ -45,8 +52,13 @@ public sealed class AppSession : IDisposable
UserDefinedContestProblems = problems;
BandPlan = settings.ToBandPlan();
ApplySpotSettings();
spotFlush = new Timer(_ => FlushSpots(), null, SpotFlushInterval, SpotFlushInterval);
}
/// How often the spots that have arrived go onto the bandmap. N1MM's own
/// queue is flushed once a second.
private static readonly TimeSpan SpotFlushInterval = TimeSpan.FromSeconds(1);
public UserPaths Paths { get; }
public Settings Settings { get; private set; }
@@ -466,17 +478,59 @@ public sealed class AppSession : IDisposable
Changed?.Invoke(this, EventArgs.Empty);
}
/// A spot the node sent, kept or dropped by the filters on the telnet
/// window.
/// A spot the node sent. It waits for the next flush rather than going
/// straight onto the bandmap.
private void TakeSpot(Spot spot)
{
if (!spotFilter.Accepts(spot))
lock (arrivingGuard)
{
return;
arriving.Add(spot);
}
Bandmap.Add(Settings.RandomizeSpots ? SpotJitter.Shifted(spot, BandPlan, jitter) : spot);
}
/// The spots that arrived since the last flush, filtered and moved about,
/// put on the bandmap in one go.
private void FlushSpots()
{
List<Spot> batch;
lock (arrivingGuard)
{
if (arriving.Count == 0)
{
return;
}
batch = [.. arriving];
arriving.Clear();
}
List<Spot> keeping = [];
foreach (Spot spot in batch)
{
if (spotFilter.Accepts(spot))
{
keeping.Add(Settings.RandomizeSpots
? SpotJitter.Shifted(spot, BandPlan, jitter, IsDupe(spot))
: spot);
}
}
if (keeping.Count > 0)
{
Bandmap.AddRange(keeping);
}
}
/// Whether the station has already been worked, which decides whether a
/// spot is moved about: N1MM leaves a dupe where it is.
private bool IsDupe(Spot spot) =>
Logging is not null && Logging.Log.Judge(new Qso
{
Id = "",
TimestampUtc = spot.AtUtc,
Call = spot.Call,
Frequency = spot.Frequency,
Mode = Position?.Mode ?? Modes.Cw,
ContestName = Logging.Contest.Name,
}) is { IsDupe: true };
private void ApplySpotSettings()
{
spotFilter = Settings.SpotFilter.ToFilter(BandPlan, Settings.Station, Countries, Calls, History);
@@ -499,6 +553,7 @@ public sealed class AppSession : IDisposable
public void Dispose()
{
spotFlush.Dispose();
DisposeRadios();
cluster?.Dispose();
network?.Dispose();