diff --git a/src/Nonemm.App/AppSession.cs b/src/Nonemm.App/AppSession.cs
index c592131..ae24741 100644
--- a/src/Nonemm.App/AppSession.cs
+++ b/src/Nonemm.App/AppSession.cs
@@ -1039,7 +1039,11 @@ public sealed partial class AppSession : IDisposable
private void ApplySpotSettings()
{
spotFilter = Settings.SpotFilter.ToFilter(BandPlan, Settings.Station, Countries, Calls, History);
- Bandmap.Lifetime = TimeSpan.FromMinutes(Math.Max(1, Settings.SpotTimeoutMinutes));
+ Bandmap.LifetimeFor = spot => TimeSpan.FromMinutes(Math.Max(
+ 1,
+ BandPlan.ModeAt(spot.Frequency) is ModeCategory.Cw or ModeCategory.Digital
+ ? Settings.CwDigitalSpotTimeoutMinutes
+ : Settings.PhoneSpotTimeoutMinutes));
foreach (OperatingPosition position in positions)
{
position.CqLeaveHertz = Settings.CqLeaveHertz;
diff --git a/src/Nonemm.App/Configuration/Settings.cs b/src/Nonemm.App/Configuration/Settings.cs
index b955c1f..d375ee2 100644
--- a/src/Nonemm.App/Configuration/Settings.cs
+++ b/src/Nonemm.App/Configuration/Settings.cs
@@ -50,8 +50,12 @@ public sealed record Settings
/// Which cluster spots reach the bandmap.
public StoredSpotFilter SpotFilter { get; init; } = new();
- /// How long a spot stays on the bandmap. N1MM's own default is 60.
- public int SpotTimeoutMinutes { get; init; } = 60;
+ /// How long a CW or digital spot stays on the bandmap.
+ public int CwDigitalSpotTimeoutMinutes { get; init; } = 10;
+
+ /// How long a phone spot stays on the bandmap. A frequency with no
+ /// boundaries in the band plan counts as phone.
+ public int PhoneSpotTimeoutMinutes { get; init; } = 30;
/// The width of the slice the bandmap shows, in hertz. Zero is the whole band
/// and minus one is the CW, digital or phone part the receiver is in.
diff --git a/src/Nonemm.App/Windows/TelnetWindow.Filters.cs b/src/Nonemm.App/Windows/TelnetWindow.Filters.cs
index 20e784f..898778c 100644
--- a/src/Nonemm.App/Windows/TelnetWindow.Filters.cs
+++ b/src/Nonemm.App/Windows/TelnetWindow.Filters.cs
@@ -48,7 +48,8 @@ public sealed partial class TelnetWindow
CallAreasBox.Text = string.Join(' ', filter.CallAreas);
BlockedSpottersBox.Text = string.Join(' ', filter.BlockedSpotters);
BlockedCallsBox.Text = string.Join(' ', filter.BlockedCalls);
- TimeoutBox.Text = session.Settings.SpotTimeoutMinutes.ToString(CultureInfo.InvariantCulture);
+ TimeoutBox.Text = session.Settings.CwDigitalSpotTimeoutMinutes.ToString(CultureInfo.InvariantCulture);
+ PhoneTimeoutBox.Text = session.Settings.PhoneSpotTimeoutMinutes.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);
@@ -74,7 +75,8 @@ public sealed partial class TelnetWindow
PreferredSpotters = Words(PreferredSpottersBox.Text),
},
RandomizeSpots = RandomizeBox.IsChecked == true,
- SpotTimeoutMinutes = Minutes(TimeoutBox.Text, session.Settings.SpotTimeoutMinutes),
+ CwDigitalSpotTimeoutMinutes = Minutes(TimeoutBox.Text, session.Settings.CwDigitalSpotTimeoutMinutes),
+ PhoneSpotTimeoutMinutes = Minutes(PhoneTimeoutBox.Text, session.Settings.PhoneSpotTimeoutMinutes),
QsySpotHertz = Minutes(QsySpotBox.Text, session.Settings.QsySpotHertz),
RecentContactSpotMinutes = int.TryParse(RecentSpotBox.Text, out int recent) && recent >= 0
? recent
diff --git a/src/Nonemm.App/Windows/TelnetWindow.axaml b/src/Nonemm.App/Windows/TelnetWindow.axaml
index 349e90a..1419c61 100644
--- a/src/Nonemm.App/Windows/TelnetWindow.axaml
+++ b/src/Nonemm.App/Windows/TelnetWindow.axaml
@@ -120,8 +120,10 @@
-
+
+
+
diff --git a/src/Nonemm.Spotting/Bandmap.cs b/src/Nonemm.Spotting/Bandmap.cs
index 710b1fa..ccdecb5 100644
--- a/src/Nonemm.Spotting/Bandmap.cs
+++ b/src/Nonemm.Spotting/Bandmap.cs
@@ -26,10 +26,14 @@ public sealed class Bandmap
this.clock = clock ?? (() => DateTime.UtcNow);
}
- /// How long a spot stays on the map. N1MM asks for the same number in
- /// minutes, on the telnet window's Filters tab.
+ /// How long a spot stays on the map when `LifetimeFor` is not set.
public TimeSpan Lifetime { get; set; }
+ /// How long a given spot stays on the map, for a different time in each
+ /// mode. N1MM asks for one number in minutes, on the telnet window's
+ /// Filters tab.
+ public Func? LifetimeFor { get; set; }
+
public event EventHandler? Changed;
public void Add(Spot spot)
@@ -85,7 +89,8 @@ public sealed class Bandmap
lock (guard)
{
List stale = spots
- .Where(pair => nowUtc - arrived.GetValueOrDefault(pair.Key, nowUtc) > Lifetime)
+ .Where(pair => nowUtc - arrived.GetValueOrDefault(pair.Key, nowUtc)
+ > (LifetimeFor?.Invoke(pair.Value) ?? Lifetime))
.Select(pair => pair.Key)
.ToList();
foreach (string key in stale)
diff --git a/tests/Nonemm.Spotting.Tests/BandmapTests.cs b/tests/Nonemm.Spotting.Tests/BandmapTests.cs
index af6d85a..c314e1b 100644
--- a/tests/Nonemm.Spotting.Tests/BandmapTests.cs
+++ b/tests/Nonemm.Spotting.Tests/BandmapTests.cs
@@ -18,6 +18,21 @@ public class BandmapTests
Assert.Equal(["JA1XYZ", "DL1ABC"], map.On(Bands.Band20M).Select(s => s.Call.Text));
}
+ [Fact]
+ public void EachSpotLivesAsLongAsItsOwnLifetime()
+ {
+ Bandmap map = new(clock: () => Now)
+ {
+ LifetimeFor = spot => TimeSpan.FromMinutes(spot.Frequency.Kilohertz < 14_100 ? 10 : 30),
+ };
+ map.Add(At("DL1ABC", 14_025));
+ map.Add(At("JA1XYZ", 14_200));
+
+ map.DropOlderThan(Now.AddMinutes(15));
+
+ Assert.Equal(["JA1XYZ"], map.All().Select(s => s.Call.Text));
+ }
+
[Fact]
public void AStationThatMovesKeepsOneEntryOnItsBand()
{