Add the session, radio control, bandmap and cluster
Nonemm.Session holds what the operator is typing, what the log says about it and what happens on Enter, with no UI toolkit behind it. Nonemm.Rig talks to hamlib's rigctld and reconnects on its own. Nonemm.Spotting reads DX cluster lines into a bandmap that drops spots after an hour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
77
src/Nonemm.Spotting/Bandmap.cs
Normal file
77
src/Nonemm.Spotting/Bandmap.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Spotting;
|
||||
|
||||
/// The stations on the band, in frequency order. A spot ages out after an hour
|
||||
/// because a bandmap is a picture of the last hour, and a stale spot costs a
|
||||
/// move to an empty frequency.
|
||||
public sealed class Bandmap
|
||||
{
|
||||
private readonly Dictionary<string, Spot> spots = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeSpan lifetime;
|
||||
|
||||
public Bandmap(TimeSpan? lifetime = null) => this.lifetime = lifetime ?? TimeSpan.FromHours(1);
|
||||
|
||||
public event EventHandler? Changed;
|
||||
|
||||
public void Add(Spot spot)
|
||||
{
|
||||
spots[Key(spot)] = spot;
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void Remove(Callsign call, Band band)
|
||||
{
|
||||
if (spots.Remove($"{call.Text}|{band.Name}"))
|
||||
{
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void DropOlderThan(DateTime nowUtc)
|
||||
{
|
||||
List<string> stale = spots
|
||||
.Where(pair => nowUtc - pair.Value.AtUtc > lifetime)
|
||||
.Select(pair => pair.Key)
|
||||
.ToList();
|
||||
foreach (string key in stale)
|
||||
{
|
||||
spots.Remove(key);
|
||||
}
|
||||
if (stale.Count > 0)
|
||||
{
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<Spot> On(Band band) =>
|
||||
spots.Values
|
||||
.Where(s => s.Band == band)
|
||||
.OrderBy(s => s.Frequency.Hertz)
|
||||
.ToList();
|
||||
|
||||
public IReadOnlyList<Spot> All() =>
|
||||
spots.Values.OrderBy(s => s.Frequency.Hertz).ToList();
|
||||
|
||||
/// The spot nearest the frequency, within the window either side. Used when
|
||||
/// the radio lands on a spot so the entry window can fill the call in.
|
||||
public Spot? Near(Frequency frequency, Frequency window)
|
||||
{
|
||||
Spot? best = null;
|
||||
long bestDistance = long.MaxValue;
|
||||
foreach (Spot spot in spots.Values)
|
||||
{
|
||||
long distance = Math.Abs(spot.Frequency.Hertz - frequency.Hertz);
|
||||
if (distance <= window.Hertz && distance < bestDistance)
|
||||
{
|
||||
best = spot;
|
||||
bestDistance = distance;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// One spot per station per band: a station that moves within the band keeps
|
||||
/// one entry rather than leaving a trail.
|
||||
private static string Key(Spot spot) => $"{spot.Call.Text}|{spot.Band?.Name}";
|
||||
}
|
||||
Reference in New Issue
Block a user