diff --git a/src/Nonemm.App/AppSession.cs b/src/Nonemm.App/AppSession.cs index 9a26178..c592131 100644 --- a/src/Nonemm.App/AppSession.cs +++ b/src/Nonemm.App/AppSession.cs @@ -346,6 +346,7 @@ public sealed partial class AppSession : IDisposable Logging.Edited += (_, change) => _ = link?.SendEditedAsync(change.Qso, change.OldCall, change.OldTimestampUtc); Logging.Deleted += (_, qso) => _ = link?.SendDeletedAsync(qso); + SpotRecentContacts(Logging.Log.Qsos); Check = new CheckWindowSources(positions[0], Calls, Bandmap); Available = new AvailableStations(positions[0], Bandmap); Save(Settings with { ContestNumber = contestNumber }); @@ -663,6 +664,7 @@ public sealed partial class AppSession : IDisposable // a run of new contacts is applied before the update that follows // it, because that update may be a clear or an edit of one of them Logging.AddRangeFromNetwork(logged); + SpotRecentContacts(logged); logged.Clear(); switch (update) { @@ -678,6 +680,34 @@ public sealed partial class AppSession : IDisposable } } Logging.AddRangeFromNetwork(logged); + SpotRecentContacts(logged); + } + + /// Puts the contacts of the last few minutes on the bandmap as local spots, + /// so a restart does not lose the stations just worked. A station the + /// bandmap already holds on that band keeps its spot. Nothing is sent to + /// the other stations or the cluster. + private void SpotRecentContacts(IEnumerable contacts) + { + if (Settings.RecentContactSpotMinutes <= 0) + { + return; + } + DateTime since = DateTime.UtcNow - TimeSpan.FromMinutes(Settings.RecentContactSpotMinutes); + HashSet<(string, Band?)> held = [.. Bandmap.All().Select(s => (s.Call.Text, s.Band))]; + List spots = []; + foreach (Qso qso in contacts.Where(q => q.TimestampUtc >= since && Logging!.Contest.IsContact(q))) + { + Spot spot = new(qso.Call, qso.Frequency, qso.TimestampUtc, SpotSource.Log); + if (held.Add((spot.Call.Text, spot.Band))) + { + spots.Add(spot); + } + } + if (spots.Count > 0) + { + Bandmap.AddRange(spots); + } } /// Starts, restarts or stops the keyer, following what the settings say. diff --git a/src/Nonemm.App/Configuration/Settings.cs b/src/Nonemm.App/Configuration/Settings.cs index f63b56d..b955c1f 100644 --- a/src/Nonemm.App/Configuration/Settings.cs +++ b/src/Nonemm.App/Configuration/Settings.cs @@ -105,6 +105,11 @@ public sealed record Settings /// boxes are wiped. N1MM uses 2 kHz. public int QsySpotHertz { get; init; } = 2000; + /// Contacts logged in the last this many minutes are put on the bandmap + /// when a contest is opened and when contacts arrive from the other + /// stations, as local spots. Zero turns it off. + public int RecentContactSpotMinutes { get; init; } = 20; + /// How far the radio can move off the CQ frequency, in hertz, before a /// running position goes back to searching. public int CqLeaveHertz { get; init; } = 3000; diff --git a/src/Nonemm.App/Windows/BandmapView.cs b/src/Nonemm.App/Windows/BandmapView.cs index 9b89813..74cffc9 100644 --- a/src/Nonemm.App/Windows/BandmapView.cs +++ b/src/Nonemm.App/Windows/BandmapView.cs @@ -50,6 +50,7 @@ public sealed class BandmapView : Control private readonly Typeface typeface = new(Fonts.Mono); private readonly Typeface boldTypeface = new(Fonts.Mono, FontStyle.Normal, FontWeight.Bold); + private readonly Typeface italicTypeface = new(Fonts.Mono, FontStyle.Italic); private IReadOnlyList placed = []; private BandmapScale scale = new(Bands.Band20M.Low, Bands.Band20M.High, 1); @@ -181,7 +182,12 @@ public sealed class BandmapView : Control new Point(ScaleX + 2, label.MarkY), new Point(LabelX - 2, label.LabelY + (LabelHeight / 2))); context.DrawText( - Text(label.Spot.Call.Text, colour, TextSize, label.Spot.IsLocal || label.Spot.IsCqFrequency), + Text( + label.Spot.Call.Text, + colour, + TextSize, + label.Spot.IsLocal || label.Spot.IsCqFrequency, + label.Spot.Source == SpotSource.Log), new Point(LabelX, label.LabelY + (LabelPadding / 2))); } @@ -221,8 +227,14 @@ public sealed class BandmapView : Control FrequencyPicked?.Invoke(this, scale.At(at.Y - Inset)); } - private FormattedText Text(string text, IBrush brush, double size, bool bold = false) => - new(text, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, bold ? boldTypeface : typeface, size, brush); + private FormattedText Text(string text, IBrush brush, double size, bool bold = false, bool italic = false) => + new( + text, + CultureInfo.InvariantCulture, + FlowDirection.LeftToRight, + bold ? boldTypeface : italic ? italicTypeface : typeface, + size, + brush); /// The form's text colour, which the theme sets in both light and dark. private IBrush Foreground() => diff --git a/src/Nonemm.App/Windows/TelnetWindow.Filters.cs b/src/Nonemm.App/Windows/TelnetWindow.Filters.cs index bc19265..20e784f 100644 --- a/src/Nonemm.App/Windows/TelnetWindow.Filters.cs +++ b/src/Nonemm.App/Windows/TelnetWindow.Filters.cs @@ -50,6 +50,7 @@ public sealed partial class TelnetWindow BlockedCallsBox.Text = string.Join(' ', filter.BlockedCalls); TimeoutBox.Text = session.Settings.SpotTimeoutMinutes.ToString(CultureInfo.InvariantCulture); QsySpotBox.Text = session.Settings.QsySpotHertz.ToString(CultureInfo.InvariantCulture); + RecentSpotBox.Text = session.Settings.RecentContactSpotMinutes.ToString(CultureInfo.InvariantCulture); CqLeaveBox.Text = session.Settings.CqLeaveHertz.ToString(CultureInfo.InvariantCulture); SpotCommentBox.Text = session.Settings.SpotComment; } @@ -75,6 +76,9 @@ public sealed partial class TelnetWindow RandomizeSpots = RandomizeBox.IsChecked == true, SpotTimeoutMinutes = Minutes(TimeoutBox.Text, session.Settings.SpotTimeoutMinutes), QsySpotHertz = Minutes(QsySpotBox.Text, session.Settings.QsySpotHertz), + RecentContactSpotMinutes = int.TryParse(RecentSpotBox.Text, out int recent) && recent >= 0 + ? recent + : session.Settings.RecentContactSpotMinutes, CqLeaveHertz = Minutes(CqLeaveBox.Text, session.Settings.CqLeaveHertz), }); Show("*** filters applied — they hold for the spots that arrive from now on", NoticeColour); diff --git a/src/Nonemm.App/Windows/TelnetWindow.axaml b/src/Nonemm.App/Windows/TelnetWindow.axaml index 516ff37..349e90a 100644 --- a/src/Nonemm.App/Windows/TelnetWindow.axaml +++ b/src/Nonemm.App/Windows/TelnetWindow.axaml @@ -126,6 +126,8 @@ + +